Translate

viernes, 22 de junio de 2012

Interrupciones (conceptos básicos)

Una interrupción es un aviso provocado por un módulo del PIC, por un cambio en el estado de un pin o un recordatorio de que ha pasado un cierto tiempo. Como su nombre indica este aviso interrumpirá la tarea que se este haciendo en ese momento y pasaremos a ejecutar una rutina de servicio o gestión de la interrupción.

Veremos un repaso de los bits y registros de control asociados a las diferentes interrupciones, como habilitarlas y como escribir rutinas de servicio (ISR). Crearemos definiciones (#define) que nos permitirán operar con las interrupciones sin tener que recordar los bits/registros asociados, a la vez que facilitarán la tarea de portar nuestro programa a otro compilador y/o microcontrolador. 

Es importante familiarizarse con el manejo de interrupciones, ya que nos evita poder manejar muchos tipos de eventos sin estar pendientes de ello. En sucesivos tutoriales veremos como el uso de interrupciones nos permite aprovechar de forma mucho más eficiente los recursos del PIC.

Archivos de código asociado a esta entrada:  int_defs.h   test_int.c 

------------------------------------------------------------------------------------------------------------

Habilitación de interrupciones:

Antes de entrar en detalles sobre cada interrupción por separado hemos de describir un par de bits (bits 7 y 6 del SFR INTCON) que tienen un efecto global sobre la activación de bloques de interrupciones. 

INTCON.GIE  -> habilita (1) o deshabilita (0) todas las interrupciones.
INTCON.PEIE -> habilita (1) o deshabilita (0) las interrupciones asociadas a módulos periféricos.

Por ejemplo, antes de poder usar la interrupción del temporizador TMR0 debemos asegurarnos de que las interrupciones globales estén habilitadas (INTCON.GIE=1).  Si lo que deseamos es usar la interrupción asociada a la recepción del puerto serie, tanto INTCON.GIE como INTCON.PEIE deben estar a 1, ya que dicha interrupción está declarada como periférica.

Para usar estos bits de una forma más conveniente incluiríamos los siguientes defines (en el programa principal o bien en un fichero .h incluido en el proyecto):

// Global flags (without priority levels)
#define enable_global_ints  INTCONbits.GIE=1
#define enable_perif_ints   INTCONbits.PEIE=1
#define disable_global_ints INTCONbits.GIE=0
#define disable_perif_ints  INTCONbits.PEIE=0

Hay varias razones para usar estas (o similares definiciones):
  • Siempre es más facil recordar enable_global_ints que acordarse de que hay que poner a 1 el bit GIE del registro INTCON.
  • Si cambiamos a otro compilador donde la forma de direccionar los bits de los registros es diferente, basta cambiar las definiciones (esto es, usar un fichero .h distinto). En el caso p.e. del compilador MikroC Pro en vez de INTCONbits.GIE usaríamos INTCON.GIE.
  • Si cambiamos a otro controlador, puede que los bits correspondientes cambien de registro y/o nombre. De nuevo, un cambio en el fichero de encabezamiento hace que no sea preciso cambiar el resto del código.

Bits asociados a cada interrupción:

Además de los bits anteriores que afectan de forma global a las interrupciones, para cada fuente de interrupción hay tres bits asociados: 
  • IE (interrupt enable): determina si la interrupción está o no habilitada. Si no lo está, aunque la condición de la interrupción se cumpla,  la interrupción no se producirá.
  • IF (interrupt flan): indica si la condición de la interrupción se ha producido.  Es responsabilidad del usuario borrar dicho bit antes de regresar de la ISR.
  • IP (interrupt priority): indica si la prioridad asociada a la interrupción es alta (1) o baja (0).  Obviamente, solo tiene efecto si está activado el modo de niveles de prioridad.
Consideremos por ejemplo la interrupción asociada la temporizador TMR0. Un temporizador es simplemente un contador que se incrementa con cada ciclo máquina (4 ciclos del oscilador). Dicho contador puede configurarse como de 8 o 16 bits. Cuando dicho contador rebosa y pasa de 0xFF a 0x00 (modo 8 bits) o de 0xFFFF a 0x0000  (modo 16 bits) la bandera IF asociada a la interrupción del TMR0 se pone a 1.  Para activar o desactivar la interrupción,  establecer su prioridad o acceder al valor de su bandera de interrupción definiríamos los siguiente macros:

#define enable_TMR0_int   INTCONbits.TMR0IE=1
#define disable_TMR0_int  INTCONbits.TMR0IE=0
#define TMR0_flag         INTCONbits.TMR0IF
#define set_TMR0_high     INTCON2bits.TMR0IP=1
#define set_TMR0_low      INTCON2bits.TMR0IP=0

Las dos primeras líneas activan o desactivan la interrupción a través del correspondiente bit de IE. La tercera define el flag (IF) asociado al temporizador como TMR0_flag.  Finalmente las dos últimas establecen la interrupción del TMR0 como de alta o baja prioridad, modificando el correspondiente bit IP.

Algunas interrupciones pueden tener algunos bits extras dedicados. Por ejemplo, en la interrupción INT0 (asociada a detectar cambios en el pin RB0) podemos especificar si la interrupción salta al pasar de nivel alto a bajo o viceversa.

Como se observa en los ejemplos anteriores todos los bits que hemos visto hasta ahora están en los registros especiales INTCON e INTCON2. Al ir añadiendo más fuentes de interrupción se han tenido que crear nuevos registros  para interrupciones de periféricos (PIEx,PIRx), lugares donde almacenar los bits de prioridades (IPRx), etc.

Podemos repetir las definiciones anteriores para el resto de las interrupciones. Cada interrupción tendría una serie de definiciones similares a las listadas anteriormente para TMR0.  Por ejemplo, para la interrupción INT1 tenemos definidas:

#define enable_INT1_int  INTCON3bits.INT1IE=1
#define disable_INT1_int INTCON3bits.INT1IE=0
#define INT1_low2high    INTCON2bits.INTEDG1=1
#define INT1_high2low    INTCON2bits.INTEDG1=0
#define INT1_flag        INTCON3bits.INT1IF
#define set_INT1_high    INTCON3bits.INT1IP=1
#define set_INT1_low     INTCON3bits.INT1IP=0


De esta forma tendríamos una forma sencilla de activar una u otra interrupción, consultar sus banderas y establecer su prioridad sin tener que recordar la posición de los diferentes bits en los registros. Lo usual es guardar dichas definiciones en un fichero que incluiríamos en nuestros proyectos. El fichero que usare en sucesivos programas es int_defs_C18.h.

Para el resto de las interrupciones tenemos similares definiciones, sin más que cambiar TMR0 o INT1 por el código de la interrupción en cuestión. Entre las interrupciones que más usaremos podemos destacar:

Temporizadores/Contadores  (TMR0, TMR1, TMR2, TMR3) 

  La interrupción se produce al rebosar y pasar por 0 los contadores asociados.

Cambios en pines  (INT0, INT1, INT2, RB)

INTx: La interrupción se produce con un cambio en el nivel de los pines RB0, RB1 y RB2 respectivamente. Es posible establecer si la interrupción se produce en el flanco ascendente o descendente (ver INT1_low2high e INT1_high2low en los ejemplos citados antes).

RB: se produce ante cualquier cambio en los pines RB4 a RB7. Al contrario que las anteriores no es posible especificar un pin o una transición determinada.

Puerto serie  (Rx,Tx)

 RX, interrupción producida con la recepción de un carácter.

 TX, interrupción de transmisión que nos avisa cuando el buffer de transmisión está libre para mandar un nuevo carácter.

Conversor Analógico/Digital (AD)

 AD, nos avisa cuando se ha completado una conversión Analógica-Digital.


Rutinas de servicio de interrupciones  (ISRs)

Como hemos visto, para que una interrupción se produzca, los siguientes bits deben estar a 1:

GIE -> Habilita todas las interrupciones

PEIE -> Necesario (además de GIE) para las interrupciones periféricas.

El bit IE (int enable) de la interrupción deseada, habilitando dicha interrupción en particular.

El bit IF (int flag) de la interrupción, indicando que la condición de la interrupción se ha producido.

Es responsabilidad del usuario activar los tres primeros bits con los correspondiente comandos enable que hemos definido. Por ejemplo para activar la interrupción del timer 0, TMR0, haríamos:

 enable_global_ints;  // Enable global ints
 enable_TMR0_int;     // Enable TMR0 int

Si deseáramos activar la interrupción de recepción del puerto serie (RX) haríamos:

 enable_global_ints;  // Enable global ints
 enable_perif_ints;   // Enable peripheral ints
 enable_RX_int;     // Enable RX int

Con los tres primeros bits en 1, cuando se cumpla una condición de interrupción el microcontrolador pondrá a 1 el correspondiente bit IF y la interrupción se producirá. El microcontrolador pasará el control a la posición 0x0008.

Cuando esto suceda es fundamental que en dicha posición tengamos un código válido para gestionar la interrupción. A dicho código se le denomina rutina de servicio de la interrupción (Interrupt Service Routine o ISR).

Las funciones mínimas de una ISR son las siguientes:

·    Determinar que interrupción se ha producido, ya que en principio todas las interrupciones se procesan en la misma rutina. Esto lo haremos chequeando que IF bit está a 1. Obviamente no será necesario chequear las flags de todas las interrupciones, sino sólo de aquellas que estén habilitadas.

·    Una vez determinada que interrupción en particular ha sucedido, ejecutar el código que sirve a dicha interrupción.

·    Finalmente, antes de volver, poner a 0 la correspondiente bandera IF. Si no lo hacemos, en cuanto devolvamos el control a la rutina principal se volverá a verificar la condición de interrupción y volveremos a entrar en la ISR.

Hemos dicho que ante una interrupción el PIC saltará a una dirección determinada. ¿Cómo podemos poner el código de la ISR en la posición de memoria adecuada? Eso va a depender del compilador.  Usualmente los compiladores nos facilitan dicha tarea teniendo una rutina de nombre predefinido para las interrupciones. Si se define una rutina con dicho nombre el compilador la pondrá en la posición adecuada al compilar por lo que se ejecutará  

Veremos como lo hacen el C18 de Microchip y el MikroC Pro de Mikroelektronica.

Compilador C18:

#pragma interrupt high_ISR
void high_ISR (void)
{
 if (TMR0_flag) // ISR de la interrupcion de TMR0
  {
   PORTCbits.RC0=1; Delay10KTCYx(255); PORTCbits.RC0=0;
   TMR0_flag=0;
  }
}

// Code @ 0x0008 -> Jumps to ISR routine
#pragma code high_vector = 0x0008
  void code_0x0008(void) {_asm goto high_ISR _endasm}
#pragma code

La primera parte del código usa  #pragma interrupt para indicarle al compilador que la rutina siguiente es una interrupción (una interrupción debe volver con una instrucción especial, Return from Interrupt, en vez de un Return normal). El nombre usado high_ISR() es arbitrario y podemos cambiarlo por el que queramos.

La rutina high_ISR() se ejecutará al producirse cualquier interrupción. Por eso lo primero que hacemos es chequear que interrupción ha causado la llamada a la ISR, consultando las posibles IF de las interrupciones posibles (las habilitadas). En este caso que tenemos una sola interrupción habilitada podríamos evitar dicha comprobación.

Una vez verificado (TMR0_flag==1) que efectivamente estamos ahí por una interrupción del TMR0 simplemente escribimos el código que deseemos ejecutar.  ¿Qué es lo que se hace en la ISR del TMR0? Poca cosa: levantamos el pin RC0, esperamos 255x10000 ciclos de reloj (unos 0.5 segundos) y ponemos de nuevo a 0 el pin RC0 antes de regresar.  El delay podría representar el tiempo que estaríamos ocupados haciendo lo que se supone que tendríamos que hacer cada cierto tiempo. Mientras veamos encendido RC0 es que estamos dentro de la ISR. Muy importante: antes de regresar reseteamos la bandera (TMR0_flag=0) para evitar entrar de nuevo en la interrupción.

La segunda parte del código usa la directiva #pragma code que nos permite posicionar un código en una dirección de memoria dada. Se define una nueva función code_0x0008 (de nuevo un nombre arbitrario) cuyo código es muy sencillo, simplemente un salto a la rutina high_ISR anterior.

Se ve que C18 deja ver lo que realmente está pasando. En la posición 0x0008 no podríamos meter una rutina muy grande (al fin y al cabo en 0x0018 podríamos tener otra rutina, la de la interrupción de bajo nivel). Lo único que metemos es un salto a la verdadera rutina de interrupción high_ISR().


MikroC Pro:

En el caso del compilador MikroC Pro, hay una rutina de nombre reservado, interrupt( ).  Veamos como escribiríamos el código anterior para MikroC Pro.

void interrupt(void// High priority interrupt
{
 if (TMR0_flag)      // TMR0 ISR
  {
   PORTC.RC0=1; delay_ms(600); PORTC.RC0=0;
   TMR0_flag=0;
  }
}

Vemos que el código es más sencillo. Basta declarar la función (reservada) interrupt y el compilador se ocupa de todo. Por debajo el compilador MikroC Pro hará lo mismo que el C18 (escribir el código en algún sitio y poner un salto en 0x0008 a esa dirección), pero la ventaja (o inconveniente según se mire) es que hace que el usuario se despreocupe de los detalles.

Nosotros seguiremos a partir de ahora la programación en C18. De hecho, las únicas diferencias eran en la forma de declarar las interrupciones, ya que el programa principal sería idéntico en ambos compiladores.

Lo único que tenemos que hacer en el main() es configurar de forma adecuada TMR0 para que salte en el tiempo deseado. Para ello damos cierto valor al registro T0CON (veremos detalles de cómo se hace en la entrada dedicada a los temporizadores).  Tras programar el temporizador lo único que queda es habilitar las interrupciones globales y la interrupción del TMR0 en particular: 

void main() {

  TRISC=0; PORTC=0x00;   // PORTC output
  T0CON = 0b10000110; // Starts TMR0, rolls over every 128x65536 cycles = 1.67 sec @ 20 Mhz
 
  enable_TMR0_int;    // Enable Timer0 interrupt
  enable_global_ints; // Enable global ints
 
  while(1);
}

Vemos que en el bucle principal no hacemos nada. Sin embargo, al ejecutar el programa veremos como el pin RC0 parpadea: cada 1.6 segundos entra la interrupción en la que RC0 se pone en 1 (LED encendido) y permanece así durante 600 msec, apagándose hasta el próximo ciclo.

La moraleja: aunque nuestro programa principal no este haciendo nada, el microcontrolador puede estar haciendo cosas a través del código asociado a las interrupciones habilitadas. Veremos la aplicación de este principio en otras aplicaciones.

Podéis comprobar que si comentáis cualquiera de las dos líneas enable el LED en RC0 permanece apagado, ya que la interrupción no se ejecuta al no estar habilitada (o al no estar habilitadas la interrupciones de forma global).

Es importante que siempre que habilitemos una interrupción tengamos definida la correspondiente función de manejo de interrupciones (posicionada en 0x0008). Si no es así, al producirse la interrupción y saltar el programa a la dirección 0x0008, al no encontrar un código valido en esa dirección el comportamiento es impredecible.




15 comentarios:

  1. Simplemente genial la explicación :)
    Muchas gracias y sigue así!!

    ResponderEliminar
  2. Hola qutal me gustaria como hacer una interrupcion usart en pic basic pro

    ResponderEliminar
  3. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  4. excelente amigo todos tus programas explicacion coo de un profesor, podrias explicar mas de os fuses como usay y no usar PLL como hacer un bootloader en Pic 18f2550 o 18f4550 usando C18 y no CCS.

    ResponderEliminar
  5. Tremendo aporte amigo, sigue así :)

    ResponderEliminar
  6. Muy buena explicación, simplemente clarisimo!!!... Gracias!

    ResponderEliminar
  7. algún ejemplo de 3 led y 3 botones cada led para cada botón

    ResponderEliminar
  8. Eres el primero al que le entiendo el uso de las interrupciones, muchas gracias

    ResponderEliminar
  9. muy buena explicacion, la primera tan clara que he visto de tantas, gracias.

    ResponderEliminar
  10. en c compiler ,me podeis ayudar ? tengo que interrumpir el timer1 con un botón .Donde tengo que colocar las banderas para el rb0 ? y cual es la instrucción o codigo?

    ResponderEliminar
  11. Gracias por explicar tan claro!!!

    ResponderEliminar
  12. Muy buena explicacion. Ahora si programo en asembler la rutina de salto en la dirección 0x0008 se haria con goto o con call estoy claro q el regreso al programa ppal se hace con retfie y no con un return. Gracias por su respuesta.

    ResponderEliminar