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.
Simplemente genial la explicación :)
ResponderEliminarMuchas gracias y sigue así!!
Hola qutal me gustaria como hacer una interrupcion usart en pic basic pro
ResponderEliminarEste comentario ha sido eliminado por el autor.
ResponderEliminarGraciaaaaaaaas!!!
ResponderEliminarexcelente 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.
ResponderEliminarTremendo aporte amigo, sigue así :)
ResponderEliminarMuy buena explicación, simplemente clarisimo!!!... Gracias!
ResponderEliminarExcelente aporte, muy claro.
ResponderEliminaralgún ejemplo de 3 led y 3 botones cada led para cada botón
ResponderEliminarEres el primero al que le entiendo el uso de las interrupciones, muchas gracias
ResponderEliminarmuy buena explicacion, la primera tan clara que he visto de tantas, gracias.
ResponderEliminaren 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?
ResponderEliminarGracias por explicar tan claro!!!
ResponderEliminarMuy 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.
ResponderEliminarBien explicado!
ResponderEliminargaaaaaaaaaa
ResponderEliminar