Sei sulla pagina 1di 76

EJERCICIOS DE

MICROCONTROLADORES EN C

1
ÍNDICE
1. Construcción de un simple secuenciador a LED
2. Decodificador de Binario a Decimal
3. Secuencia de luces. Utilizando operadores aritméticos
4. Conversión A/D utilizando REF02
5. Uso de la interrupción externa por RB0/INT
6. Uso de la Interrupción por cambio de estado en RB4-RB7
7. Uso del TMR0 como contador
8. Uso del TMR0 como temporizador
9. Uso del TMR1 como Temporizador
10. Uso de la memoria EEPROM interna
11. Control de varias interrupciones
12. Ejemplo comunicación serie I2C entre un PIC y la memoria
EEPROM 24LC256A
13. Comunicación serie entre dos PICs con la USART

2
1. Construcción de un simple secuenciador a LED
Realizar un circuito para encender en forma secuencial un diodo LED en RB0, cuya frecuencia
de intermitencia es de 1 seg.

El circuito a ser realizado se presenta a continuación:

El código de este ejemplo en Assembler está disponible en el capítulo “Los Puertos Paralelos de
Entrada/Salida”, LED.ASM, y a continuación se muestra en lenguaje C:
/******************************************************************************
* *
* DESCRIPCIÓN: Decodificador de binario a decimal *
* Led.c *
* *
******************************************************************************/

#include <16F877.h>

#fuses XT,NOWDT,NOPROTECT

#use delay(clock=4000000)
#use fixed_io(b_outputs = PIN_B0)

void main()
{

while(1) {
output_high(PIN_B0);
delay_ms(1000);
output_low(PIN_B0);
delay_ms(1000);
}

Análisis del código LED.C

Se va analizar línea por línea el contenido del source LED.C, desde de la primera línea de
código.

o #include esta directiva de C hace que el compilador incluya en el archivo fuente el


texto que contiene el archivo especificado, en este caso la directiva que permite
definir el PIC a utilizar.
#include <16F877.h>

3
o #fuses es una directiva del compilador C que permite definir la palabra de configuración del
PIC, esta directiva define qué fusibles deben activarse en el dispositivo cuando se programe.
En este caso se informa al compilador que se usa un reloj en modo XT, no Watch Dog y no
protegido el código:

#fuses xt,NOWDT,NOPROTECT

o La directiva #use delay sirve establecer el reloj con el cual va a trabajar el PIC.

#use delay(clock=4000000)

o La directiva #use fixed causará que el compilador genere código para hacer que un pin de
I/O sea entrada o salida cada vez que se utiliza.

#use fixed_io(b_outputs = PIN_B0)

NOTA: No son necesaria las directivas: #include, #fuses y #use delay, porque en el archivo
LED.H se configuran automáticamente al crear el proyecto.

o void main() es la función principal del programa, en el cual hay un lazo infinito que pone en
alto la salida RB0, a continuación hay un delay de 1 seg, y lo mismo para cuando la salida se
pone en bajo:

while(1) {
output_high(PIN_B0);
delay_ms(1000);
output_low(PIN_B0);
delay_ms(1000);
}

4
2. Decodificador de Binario a Decimal
Decodificador de binario a decimal, mostrando los resultados en un display de siete segmentos.

CIRCUITO:

U1 RN1
13 33 1 16
OSC1/CLKIN RB0/INT
14 34 2 15
OSC2/CLKOUT RB1
35 3 14
RB2
2 36 4 13
0 3
RA0/AN0
RA1/AN1
RB3/PGM
RB4
37 5 12
4 38 6 11
1 5
RA2/AN2/VREF-/CVREF
RA3/AN3/VREF+
RB5
RB6/PGC
39 7 10
6 40 8 9
1 7
RA4/T0CKI/C1OUT
RA5/AN4/SS/C2OUT
RB7/PGD
15 RX8
RC0/T1OSO/T1CKI
8 16
RE0/AN5/RD RC1/T1OSI/CCP2
9 17
RE1/AN6/WR RC2/CCP1
10 18
RE2/AN7/CS RC3/SCK/SCL
23
RC4/SDI/SDA
1 24
MCLR/Vpp/THV RC5/SDO
25
RC6/TX/CK
26
RC7/RX/DT
19
RD0/PSP0
20
RD1/PSP1
21
RD2/PSP2
22
RD3/PSP3
27
RD4/PSP4
28
RD5/PSP5
29
RD6/PSP6
30
RD7/PSP7
PIC16F877A

Comentario:

Circuito sencillo que hace las funciones de un decodificador de binario a decimal de un dígito y
muestra los resultados en un display de siete segmentos de cátodo común, lo que quiere decir que
el PIC debe proporcionar valores positivos en su salida de la puerta B.

La relación entre las patillas del display y el valor de las letras de cada segmento es la siguiente:

luego se puede hacer una pequeña tabla de verdad donde se muestre por cada combinación en la
entrada que patillas del PIC se tienen que activar:

5
La lógica del programa lo único que hace es chequear el estado de las entradas, en este caso
representadas por los interruptores A0, A1 y A2 y activar el conjunto de patillas
correspondientes en el PIC que alimentarán los ánodos del display.

El código del ejemplo se muestra a continuación:

6
/******************************************************************************
* *
* DESCRIPCIÓN: Decodificador de binario a decimal *
* Bin_Dec.c *
* *
******************************************************************************/

#include <16F877.h>

#use delay( clock=4000000 )

#fuses XT, NOWDT

#byte port_A = 0x05 // Dirección del port A.


#byte port_B = 0x06 // Dirección del port B.

void main()
{
set_tris_b( 0x00 ); // Se configura port B como salida.
set_tris_a( 0x3F ); // Se configura port A como entrada

port_B = 0x00; // Ningún segmento a ON

while( true )
{
switch( port_A )
{
case 0:
port_B = 0x3F;
break;
case 1:
port_B = 0x06;
break;
case 2:
port_B = 0x5B;
break;
case 3:
port_B = 0x4F;
break;
case 4:
port_B = 0x66;
break;
case 5:
port_B = 0x6D;
break;
case 6:
port_B = 0x7D;
break;
case 7:
port_B = 0x07;
break;
}
}
}

7
3. Secuencia de luces. Utilizando operadores aritméticos
Este ejemplo simula las luces del coche fantástico, en cuanto a lo que hace no tiene nada de
especial, este es un ejemplo que sirve para ver cómo utilizar los operadores en los programas.

Comentario:

En este ejemplo una multiplicación por 2 (en binario 10) hace desplazar el bit que está a uno
hacia la izquierda. Al dividir entre 2 desplazamos el bit a 1 hacia la derecha, con lo que tenemos
una secuencia parecida al del coche fantástico.

En la línea 4: #byte puerto_b=6 --> creamos un identificador llamado puerto_b para referenciar
el registro de proposito específico (SFR) con dirección 0x06. Esta es una forma de acceder a los
puertos del PIC, pero hay más. Se verán todas las formas cuando se vea el tema de los punteros.

En la línea 8: set_tris_b(0x00); --> configuramos el puerto b del PIC como salida digital.

En la línea 9: puerto_b=1; --> asignamos el valor 1 (en binario 0b00000001) al puerto b, o sea
RB0=1 los demás bits del puero B a cero.

Esquema del circuito:

8
U1 RN1 U2
13 33 1 16 1 20
OSC1/CLKIN RB0/INT
14 34 2 15 2 19
OSC2/CLKOUT RB1
35 3 14 3 18
RB2
2 36 4 13 4 17
RA0/AN0 RB3/PGM
3 37 5 12 5 16
RA1/AN1 RB4
4 38 6 11 6 15
RA2/AN2/VREF-/CVREF RB5
5 39 7 10 7 14
RA3/AN3/VREF+ RB6/PGC
6 40 8 9 8 13
RA4/T0CKI/C1OUT RB7/PGD
7 9 12
RA5/AN4/SS/C2OUT
15 220 10 11
RC0/T1OSO/T1CKI
8 16
RE0/AN5/RD RC1/T1OSI/CCP2
9 17 LED-BARGRAPH-GRN
RE1/AN6/WR RC2/CCP1
10 18
RE2/AN7/CS RC3/SCK/SCL
23
RC4/SDI/SDA
1 24
MCLR/Vpp/THV RC5/SDO
25
RC6/TX/CK
26
RC7/RX/DT
19
RD0/PSP0
20
RD1/PSP1
21
RD2/PSP2
22
RD3/PSP3
27
RD4/PSP4
28
RD5/PSP5
29
RD6/PSP6
30
RD7/PSP7
PIC16F877A

El código del ejemplo se muestra a continuación:

/******************************************************************************
* *
* DESCRIPCIÓN: Coche fantástico (utilizando operadores matemáticos) *
* Secuencia.c *
* *
******************************************************************************/

#include <16F877.h>

#use delay(clock=4000000)

#fuses XT,NOWDT

#byte puerto_b=0x06

void main() {

set_tris_b(0x00);
puerto_b=1;
delay_ms(1000);
while(true)
{
while(puerto_b<=0b01000000)
{
puerto_b*=2;
delay_ms(1000);
}
while(puerto_b>=0b00000010)
{
puerto_b/=2;
delay_ms(1000);
}
}
}

9
4. Conversión A/D utilizando REF02
El conversor A/D necesita 2 tensiones de referencia para su funcionamiento, Vref+ y Vref-.
Dichas tensiones serán proporcionadas por el circuito REF02, ya que este nos da tensiones
muy exactas. En este ejemplo hemos decidido que no vamos a utilizar Vref-, entonces
conectamos los 0V de REF02 directamente a tierra, lo más cerca posible a Vss para evitar
ruidos. (El sensor también lo pondremos lo más cerca posible de Vss)

El conversor lo utilizamos para comparar los niveles de tensión con la salida analógica del
sensor y así poder muestrearla y cuantificarla. La tensión proporcionada por el
REF02 la conectamos al conversor A/D por la patilla AN3.

Programa en C, de configuración del conversor A/D

Hemos estructurado el programa en cuatro funciones:

• Inicia_AD(): Esta función nos inicializa los registros ADCON0 y ADCON1. En


ADCON0 definimos una frecuencia de Fosc/32 , ya que vamos a utilizar una frecuencia
de 20Mhz., el canal por defecto será el 0 y ponemos el conversor en marcha. En
ADCON1 definimos que el bit de mayor peso sea ADRESSH y decimos que dos entradas
sean analógicas.

• Selecciona _A/D(): Esta función sirve para seleccionar el canal por donde queramos
hacer la conversión (hay 8 canales posibles), en este caso hemos elegido el canal 1 que es
donde hemos conectado el sensor.

• Arranca_A/D(): Esta función pone el bit GO/DONE en 1 para que comience a convertir.

• Leer_A/D: Esta función me devuelve el resultado de la conversión, en este caso


me lo devuelve en la variable resultado

10
Tabla de configuraciones de la entrada del A/D

El código del ejemplo se muestra a continuación:

/******************************************************************************
* *
* DESCRIPCIÓN: Conversión Análoga Digital *
* ConversorAD.c *
* *
******************************************************************************/

#include <16f877.h>
#include <reg.h>

#fuses HS, NOWDT, NOPROTECT, NOPUT, NOBROWNOUT, NOLVP

#use delay(clock=4000000)

#byte port_B = 0x06 // Dirección del port B.

void inicia_AD(void);
void selecciona_AD(unsigned num);
void arranca_AD(void);
unsigned long leer_AD(void);

void main()
{
unsigned canal;
unsigned long resultado;

set_tris_b( 0x00 ); // Se configura port B como salida.

port_B=0x00;

inicia_AD(); //llamamos a la funcion inicia_AD


while(TRUE)
{
canal=0b00000001; //indicamos el canal de entrada A1
selecciona_AD(canal); //llamamos a la función selecciona_AD dándole
//como parametro el canal
arranca_AD(); //llamamos a dicha función
11
resultado=leer_AD(); //leer_AD nos devuelve el valor de la conversión
//y lo guarda en resultado
port_B=resultado;
}
}

void inicia_AD(void)
{
ADCON0=0b10000001; //Fosc/32, y channel 0 y 1
ADCON1=0b10000101; //4 ultimos bits dependiendo de la tabla,
//y alineación derecha
}

void selecciona_AD(unsigned num)


{
char aux;

aux=ADCON0 & 0b11000111; //pongo a 0 el canal de conversion


ADCON0=aux | (num<<3); //activamos el canal que se pasa
}

void arranca_AD(void)
{
bit_set(ADCON0,2); //ponemos en marcha el conversor
}

unsigned long leer_AD(void)


{
char alto,bajo;
long total;

while (bit_test(ADCON0,2)); //aquí esperamos a que acabe de convertir


bajo= ADRESL;
alto = ADRESH;
total= (long)alto<<8 | bajo; //Hacemos una or de bajo y de alto convertido
//a long y desplazado 8 bits.
return(total);
}

El archivo “reg.h” tiene las definiciones de los registros FSR utilizados en la aplicación, el cual
se muestra a continuación:
#byte ADCON0=0x1F
#byte ADCON1=0x9F
#byte ADRESL=0x9E
#byte ADRESH=0x1E

#bit ADON=0x1F.2 //Identificadores de bits

12
5. Uso de la interrupción externa por RB0/INT
En este ejemplo vamos a ver el uso de la interrupción externa a través de la patilla RB0/INT,
para ello vamos a utilizar el PIC 16f877, aunque si utilizas otro PIC de mayores prestaciones el
proceso será el mismo.

En cuanto a los pines del PIC la señal externa para producir la interrupción en el PIC será a
través de de la patilla RB0/INT y se podrá determinar por software que flanco de la señal
producirá la interrupción, el de subida (cuando la señal pasa de un nivel 0 a 1) ó el de bajada
(cuando pasa de 1 a 0).

Vamos a ver ahora los registros específicos (SFR) que nos proporciona el PIC para el control de
esta interrupción.

OPTION

#RBPU INTED TOCS TOSE PSA PS2 IPS1 PS0


Bit 7 Bit 0

INTCON

GIE PEIE T0IE INTE RBIE T0IF INTF RBIF


Bit 7 Bit 0

 INTED: flanco activo interrupción externa.


1 la interrupción se producirá en el flanco ascendente.
0 la interrupción se producirá en el flanco descendente.

 GIE: (Habilita las interrupciones globalmente). Este bit permite que cualquier
interrupción sea posible. Para poder usar cualquier interrupción hay que habilitarla
globalmente e individualmente.

 INTE: Permiso de interrupción por activación de la patilla RB0/INT


1 permite la interrupción
0 prohíbe la interrupción

 INTF: bit de señalización (solo lectura) de interrupción externa RB0/INT

Para ver como CCS gestiona estos recursos, vamos a crear el siguiente circuito en Proteus:

13
El circuito es una alarma sencilla, lo que tiene que hacer es lo siguiente:

Cuando se pulse el sensor de alarma y no esté cerrado el interruptor de desactivación de alarma,


se pondrá a uno la patilla RB7 del PIC el cual polarizará directamente el transistor Q1, que a su
vez activará el Micro-relé RL1 (Relé auxiliar), este a su vez activará un relé de potencia que
activará una alarma conectada a 220V.

Cuando queremos activar una carga con una potencia importante, es necesario adaptar el circuito
de salida para poder controlar con los pocos miliamperios que nos da el PIC la carga que
queramos. Hay muchas formas de hacer esto, se puede utilizar un par de transistores en
configuración Darlington y atacar directamente el relé de potencia ó no utilizar relés y controlar
la carga por medio de un Triac de potencia, lo podrás hacer como quieras.

Una vez activada la alarma, permanecerá activa hasta que no la desactivemos por medio del
interruptor de desactivación de alarma (interruptor cerrado).

Ahora vamos a crear el programa en C, para ello vamos a crear un nuevo proyecto a través del
asistente, de esta forma vamos viendo las posibilidades que nos da esta herramienta,
configuraremos las opciones según se muestra en las siguientes figuras:

14
Los demás valores dejamos por defecto.

Una vez creada la plantilla con el asistente, añadiremos las siguientes líneas de código para hacer
nuestro programa funcional:

15
Nota: El asistente genera dos archivos de código fuente, el .h y el .c esta es la forma correcta de
trabajar en C, la famosa frase de divide y vencerás es perfectamente aplicable a la programación
en C, se sigue utilizando un solo archivo para todo el código es porque de momento los archivos
son muy pequeños y resulta más cómodo incluir una imagen en el sitio en vez de dos. Realmente
cuando tengamos un proyecto grande, estará formado por varios archivos .h y .c

Comentario del programa

Al igual que la directiva #byte para mapear un registro en la memoria RAM y utilizarlo como
una variable más, con la directiva #bit identificador registro.bit podemos mapear un bit en
concreto de un registro. Aunque ya vimos que CCS incluye funciones para el manejo de bits,
está opción es muy cómoda de utilizar y hará que nuestro código sea más portable para utilizar
en otros compiladores.

#int_EXT
void EXT_isr(void)
{
//pon aquí el código de la interruptor
}

Esta parte la crea el asistente e incluye la función de interrupción, donde tendremos que incluir el
código que queremos que se ejecute, cuando se active la interrupción externa por la patilla RB0.
En nuestro caso lo que hace es comprobar si el interruptor de desactivación está abierto, si es así,
activará la alarma conectada a RB7. Y permanecerá en un bucle infinito hasta que la
desactivemos por medio del interruptor.

Si el interruptor está cerrado y pulsamos el sensor de alarma la interrupción se producirá


igualmente, pero al no cumplirse la condición del if no ejecutará ninguna sentencia.

16
Dentro del bloque principal main, tenemos las sentencias que habilitan las interrupciones
globales y la particular a RB0.

enable_interrupts(GLOBAL);
globalenable_interrupts(INT_EXT);

Nota: Por defecto la activación de la interrupción se produce en el flanco de subida, si queremos


que sea en el flanco descendente podemos hacerlo por medio de la función:

ext_int_edge(flanco)

Ejemplo:

ext_int_edge(L_TO_H); --> flanco de subida.


ext_int_edge(H_TO_L); --> flanco de bajada.

Si no queremos utilizar las funciones de CCS. Podemos modificar directamente el bit INTEDG
Ejemplo:

#bit INTEDG=0x81.6

INTEDG=1; //flanco ascendente


INTEDG=0; //flanco descendente

Para ver como realmente CCS maneja estos registros lo podemos ver si ejecutamos el programa
paso a paso en Proteus, si paramos la simulación en el bucle while de la función principal
obtendremos lo que se muestra en la figura de abajo. Donde se puede ver el estado de los bits de
configuración:

 INTEDG=1 (Flanco de activación ascendente, el que tiene por defecto)


 GIE= 1 (Permitidas las interrupciones globales)
 INTE=1 (Permiso de activación interrupción RB0/INT)
17
 INTF=0 (Señalización de interrupción)

Si ponemos un Breakpoint en el if que hay dentro de la rutina de interrupción y pulsamos el


sensor de alarma para activar la interrupción los valores de los bits serán estos:

 INTED=1 (Flanco de activación ascendente, el que tiene por defecto)


 GIE= 0 (No permite otra interrupción mientras no se salga de esta)
 INTE=1 (Permiso de activación interrupción RB0/INT, da igual que este a 1 por que
la global está a 0)
 INTF= 1(Señalización de interrupción)

Al salir de la interrupción los bits de configuración vuelven a quedar como al principio, a la


espera de que se vuelva a producir otra interrupción.

El bit de señalización INTF no lo hemos usado en este ejemplo, pero en cualquier otro ejemplo
podremos leer su valor y utilizarlo para lo que queramos.

El código del ejemplo se muestra a continuación:

/*--------------------------------------------------------*\
| Ejemplo uso de interrupción externa por RB0 |
| |
| InterrupcionRB0.c |
\*--------------------------------------------------------*/

#include <16F877.h>

#FUSES NOWDT //No utilizar el timer Watch Dog


#FUSES XT //Oscilador XT <= 4mhz
#FUSES NOPUT //No timer en la alimentación
#FUSES NOPROTECT //codigo no protegido

#bit RB0=0x06.0 //Identificadores de bits


#bit RB1=0x06.1
#bit RB7=0x06.7

#use delay(clock=4000000)//Frecuencia de reloj

#int_EXT
void EXT_isr(void){
if (RB1) //Si el interruptor de desactivación está abierto
{
RB7=1; // activa la alarma conectada en RB7
while(RB1); //mientras el RB1=1 bucle infinito
RB7=0; //cuando RB1=0, se desacactiva la alarma
}
}

void main(){
set_tris_b(0b01111111); //configura la puerta B
RB7=0; //inicializo RB7
enable_interrupts(GLOBAL); //habilito interrupción global
enable_interrupts(INT_EXT); //habilito interrupción externa
ext_int_edge( H_TO_L );//habilito el flanco interrupción de H a l
while(true); //Bucle infinito hasta interrupción
}

18
6. Uso de la Interrupción por cambio de estado en RB4-RB7
Vamos a ver un ejemplo del uso de la interrupción por cambio del estado lógico en alguna de las
4 líneas de más peso de la puerta B (RB7-RB4) del PIC. Para ello dichas líneas tienen que estar
previamente configuradas como entradas. Este recurso hardware es muy utilizado para el control
de teclados.

El registro del PIC encargado de su configuración es INTCON:

INTCON

GIE PEIE T0IE INTE RBIE T0IF INTF RBIF


Bit 7 Bit 0

Como siempre debemos habilitar la interrupción global y la particular:

GIE: Permiso global de interrupción.


1 – Permite todas las interrupciones cuyos bits particulares de permiso lo permitan.
0 – Prohíbe todas las interrupciones

RBIE: Permiso de interrupción por cambio de estado en RB7:RB4


1 – Permite la interrupción
0 –Prohíbe está interrupción

RBIF: Señalización ó flag por cambio de estado de las patillas RB7:RB4.


1 – indica que la interrupción se ha producido.
0 – indica que no se ha activado.

Como de costumbre para ver como CCS gestiona este recurso creamos el siguiente esquema en
Proteus:

19
Se trata de la gestión de un mini teclado formado solo por cuatro teclas representadas por los
pulsadores del 1 al 4. El valor en decimal de la tecla pulsada se mostrará en un display de
cátodo común.

En esta ocasión se utiliza el PIC 16F877, a nivel de código lo único que hay que cambiar con
respecto a si utilizáramos el 16f84A es el include donde están definidos los registros del PIC y
los fusibles correspondientes si los hemos incluido, pero en este caso hay una razón más para no
utilizar el 16f84 que se explicará en el comentario del programa.

Si creamos el programa con el asistente, además de seleccionar el PIC correspondiente, tenemos


que marcar en Interrupciones la opción de permitir la interrupción particular por cambio en B4-
B7, según se muestra en la figura de abajo:

El código del programa generado por el asistente tendremos que dejarlo de la siguiente manera:

/*--------------------------------------------------------*\
| Recursos del PIC. Uso de la interrupción por cambio |
| en las patillas RB4-RB7 del PIC. |
| InterrupcionRB4_7.c |
\*--------------------------------------------------------*/

#include <16F877.h>

//Configuración de los fusibles.


#FUSES NOWDT, XT, NOPUT, NOPROTECT, NOBROWNOUT, NOLVP, NOCPD, NOWRT, NODEBUG

#use delay(clock=4000000) //Frecuencia de reloj 4MHz

#byte puerto_B = 0x06 // Identificador para el puerto B.


#byte puerto_D = 0x08 // Identificador para el puerto C.

#int_RB
void RB_isr(void) //función de interrupción
{
switch(puerto_B)
{
case 239:
puerto_D = 0x06;
break;
case 223:
puerto_D = 0x5B;

20
break;
case 191:
puerto_D = 0x4F;
break;
case 127:
puerto_D = 0x66;
break;
default:
break;
}
}

void main()
{
set_tris_b(0xFF); //Puerto B como entrada
set_tris_d(0x00); //Puerto D como salida.

enable_interrupts(INT_RB); //habilita interrupción particular


enable_interrupts(GLOBAL); //habilita interrupción global

puerto_D=0x00; //inicializo puerto D

while(True)
{
//código principal
}
}

Nota: el asistente nos incluye algunas líneas más, sobre opciones de configuración de recursos
que no estamos utilizando y que están deshabilitadas, puedes dejarlas ó quitarlas, pero es una
buena costumbre si utilizar el Wizard el quitar el código que no sea necesario para el
funcionamiento de nuestro programa.

Comentario: Dentro de la función principal configuramos el puerto B como entradas digitales,


(recordar que es un requisito que RB4-RB7 estén configuradas como entradas para la utilización
de esta interrupción) y el puerto D como salida ya que es donde conectaremos el display de siete
segmentos (como es de cátodo común, sus entradas serán activas con nivel alto).

Habilitamos la interrupción global y particular, por medio de las siguientes instrucciones:

enable_interrupts(INT_RB);
enable_interrupts(GLOBAL);

e inicializamos el puerto D con el valor 0.

Luego viene un bucle while infinito, la ejecución del programa permanecerá aquí, hasta que se
produzca la interrupción.

Dentro de la función de interrupción chequeamos el valor en decimal del puerto B por medio de
una sentencia de control switch y según sea la tecla pulsada mostramos su valor en el display.

Los cuatro bits menos significativos de la puerta B en el circuito están fijados a uno
permanentemente, por ejemplo si se pulsa el pulsador 1 sonemos RB4=0 y el valor binario a la
entrada de la puerta B será:

RB7 RB6 RB5 RB4 RB3 RB2 RB1 RB0


1 1 1 0 1 1 1 1
21
Que en decimal tendrá un valor de 239, de la misma forma determinamos el valor para las otras
posibles entradas.

En la tabla de abajo se muestran los valores a utilizar para configurar el display de siete
segmentos en cátodo común.

Valor Valor
Display Dígito G F E D C B A
Hex. Dec.
0 0 1 1 1 1 1 1 3F 63
1 0 0 0 0 1 1 0 06 6
2 1 0 1 1 0 1 1 5B 91
3 1 0 0 1 1 1 1 4F 79
4 1 1 0 0 1 1 0 66 102
5 1 1 0 1 1 0 1 6D 109
6 1 1 1 1 1 0 1 7D 125
7 0 0 0 0 1 1 1 07 7
8 1 1 1 1 1 1 1 7F 127
9 1 1 0 1 1 1 1 6F 111

Para ver como CCS gestiona está interrupción, simulamos el circuito en Proteus cargando el
archivo .cof en el PIC, para que nos permita la simulación paso a paso, al igual que hicimos con
el ejemplo de la interrupción externa por RB0/INT.

Para parar el programa dentro de la función de interrupción podemos poner un breakpoint


dentro de cualquiera de las sentencias case.

El valor del registro INTCON según la secuencia del programa será la siguiente:

Antes de producirse la interrupción:

 GIE= 1 (Permitidas las interrupciones globales)


 RBIE=1 (Permiso de interrupción por cambio de estado en RB7:RB4)
 RBIF=0 (Señalización por cambio de estado de las patillas RB7:RB4 a nivel bajo)

Al producirse la interrupción:

 GIE= 0 (No Permitidas las interrupciones globales)


 RBIE=1 (Permiso de interrupción por cambio de estado en RB7:RB4, me da igual
como este porque la global está deshabilitada)
 RBIF=1 (Señalización por cambio de estado de las patillas RB7:RB4 a nivel alto)

Al salir de la interrupción:

 GIE= 1 (Permitidas las interrupciones globales)


 RBIE=1 (Permiso de interrupción por cambio de estado en RB7:RB4)
22
 RBIF=0 (Señalización por cambio de estado de las patillas RB7:RB4 a nivel bajo)

Hasta aquí CCS gestiona la interrupción por cambio de estado en RB4:RB7 de forma similar a
como lo hacía con la interrupción externa RB0/INT, es decir, gestionando la habilitación del bit
GIE y controlando el valor del flag RBIF, según se esté dentro o fuera de la función de
interrupción, todo ello de forma transparente para el programador.

Pero en este caso hay un paso que los creadores del compilador se han pasado por alto y obliga al
programador a tener en cuenta cuando se trabaje con este tipo de interrupción y es la siguiente:

Si nos vamos al datasheet del PIC 16f877, en lo que respecta al control de esta interrupción
podemos ver lo siguiente:

"Four of the PORTB pins, RB7:RB4, have an interrupton-


change feature. Only pins configured as inputs can
cause this interrupt to occur (i.e., any RB7:RB4 pin
configured as an output is excluded from the interrup ton-
change comparison). The input pins (of RB7:RB4)
are compared with the old value latched on the last
read of PORTB. The “mismatch” outputs of RB7:RB4
are OR’ed together to generate the RB Port Change
Interrupt with flag bit RBIF (INTCON<0>)."

De aquí se deduce, que es necesario una lectura (ó escritura) del puerto B cuando se produzca
esta interrupción, en el ejemplo que hemos hecho no hay problema ya que nada más entrar en la
función de interrupción leemos el estado del puerto por medio de la sentencia switch (puerto_B).
Pero imaginaros el ejemplo de la alarma que hicimos por activación de la interrupción RB0/INT,
cuando la alarma está desactivada y pulsamos el sensor de alarma, la secuencia del programa
entra en la función de interrupción pero no hace nada dentro de ella, si intentáramos implementar
ese mismo circuito pero con cuatro sensores de alarma y utilizando la interrupción por cambio de
estado en RB4:RB7, si no incluimos una lectura del puerto dentro de la función de interrupción,
aunque no nos sirva para nada, el programa se vuelve loco y no funciona bien este recurso del
PIC.

Otro problema que se encuentra, es al realizar el ejemplo con el PIC16f84A. Al simular el


circuito con Proteus aparece el siguiente error:

violation access error in module <UNKNOW>

La versión de compilador que se utilizó para el ejemplo fue la 4.038 y la de Proteus la 7.1 SP2.
La causa del error se debe a un bug del compilador que se producía al incluir en el código esta
interrupción y utilizar el archivo .hex ó .cof resultante en Proteus. Si en vez del PIC 16f84A se
utiliza otro, como el 16F877, todo funciona correctamente. Por lo que se sabe, este bug está ya
corregido en versiones posteriores del compilador. Recordar que el error se produce al simular en
Proteus, no al compilar el código.

23
7. Uso del TMR0 como contador
Vamos a ver en este ejemplo como utilizar el TIMER 0 con una frecuencia de reloj externa al
microcontrolador, la señal externa la aplicaremos, como no, a la patilla RA4/TOCKI del PIC,
dicha señal la utilizaremos para generar una interrupción a través del TIMER0 cada segundo, en
la función de interrupción implementaremos el código necesario para hacer parpadear un Led en
la patilla RB7 del PIC.

La frecuencia de la señal de reloj externa que utilizaremos será de 400 Hz y el Timer 0 lo


configuraremos para que empiece a contar en el flanco de subida de la señal de reloj, el circuito
que tenemos que implementar será el siguiente:

Bien, primeramente vamos a ver como se tiene que configurar el registro OPTION para que el
TMR0 trabaje de esta forma:

Primeramente el bit TOCS tenemos que ponerlo a 1, recordar que:

 Si TOCS=1, el TMR0 actúa como contador.


 Si TOCS=0, el TMR0 actúa como temporizador

Luego vamos hacer nuestros cálculos, teniendo en cuenta que queremos tener una interrupción
cada segundo, si os fijáis en la formula de abajo es parecida a la que vimos en el ejemplo
anterior, a excepción de que la frecuencia de reloj externa no está multiplicada por 4. Al igual
que antes elegimos un Prescaler, en este ejemplo 4 (podíamos a ver elegido otro cualquiera que
estuviera en la tabla) y calculamos el valor con el que tenemos que inicializar el TMR0 para que
se produzca su desbordamiento cada segundo.

24
Si despejamos el valor que tenemos que cargar en el TMR0 nos sale un valor de 156 que en
hexadecimal es de 0x9C.

La función en C para configurar el TMR0 lógicamente es la misma que en el ejemplo anterior


pero con parámetros diferentes:

setup_timer_0(RTCC_EXT_L_TO_H|RTCC_DIV_4);

Las palabras utilizadas para declarar los parámetros definen ya su significado. Pero para ver
directamente lo que hace la función, podemos ejecutar el programa paso a paso, o poner un
breakpoint en la línea siguiente a la instrucción de arriba, abrir la ventana de visualización de los
registros internos del PIC que nos ofrece el entorno de Proteus y ver cómo queda el registro
OPTION después de ejecutar la sentencia:

Como vemos en los registros del PIC, el bit 5 del registro OPTION está a 1 (reloj externo) y los
bits PS2:PS0 tienen el valor 001 que corresponde a un preescaler de 1:4 que es el que habíamos
elegido.

La señal de reloj externa la podemos simular por medio de Proteus, si seleccionamos Generator
Mode -> DCLOCK nos saldrá una ventana que configuraremos según se muestra en la figura de
abajo:

25
Si analizamos la tensión en el ánodo del Led mediante una gráfica, observamos que la señal
cuadrada generada a partir de la interrupción en el pic tiene entre el flanco de subida y el flanco
de bajada una diferencia en el tiempo de 1 s que es el valor que queríamos.

Si utilizáis el asistente para generar el esqueleto de la aplicación, tal como hicimos en el ejemplo
anterior el timer hay que configurarlo de la siguiente forma:

26
Veis que sale un Overflow de 2.5 s, esto es lo que saldría en la formula si dejamos al TMR0 que
empiece a contar desde 0.

El código del ejemplo será el siguiente:

#include <16F877A.h>
#fuses XT,NOWDT
#use delay(clock=4000000)

#bit RB7=0x6.7 //definimos RB7 como el pin 7 de la puerta B

#int_TIMER0 //la siguiente función tiene que ser la de interrupción del TMR0
void TIMER0_isr(void) //function interrupción TMR0
{
if (RB7==0)
{
RB7=1;
set_TIMER0(0x9C); //inicializa el timer0
}
else
{
RB7=0;
set_TIMER0(0x9C); //inicializa el timer0
}

void main()
{
setup_timer_0(RTCC_EXT_L_TO_H|RTCC_DIV_4);
enable_interrupts(INT_TIMER0);
enable_interrupts(GLOBAL);

set_tris_b(0b01111111); //configura RB7 como salida el resto como entrada


RB7=0; //inicializo el bit RB7 a 0;

set_TIMER0(0x9C); //inicializa el timer0


//el bucle infinito es necesario ya que si el micro entra en sleep
//desactiva la interrupción del TMR0
while(true);
}

27
Como veis no he utilizado el PIC 16F84 sino el 16F877A pero es exactamente igual lo único que
hay que cambiar es la directiva #include<16f84.h> por #include <16f877A.h>. Microchip hace
coincidir por compatibilidad las direcciones de memoria de los registros de los dispositivos de
inferior categoría, de esta manera con solo cambiar una línea de código podemos emigrar nuestra
aplicación a otro PIC de mayor capacidad.

Bueno a lo mejor alguno piensa que para hacer parpadear un Led no hace falta tanta historia.
Pero no se trata del que se hace, sino del como se hace, el contenido de la función de
interrupción se puede cambiar por otro cualquiera según lo que queramos hacer en cada
programa.

Una modificación que se podría hacer sería obtener la señal de reloj externa por medio de un
circuito digital para poder implementar, si se quiere, el circuito en una placa.

28
8. Uso del TMR0 como temporizador
En esta práctica vamos a ver cómo utilizar el Timer0 con el compilador CCS.

Pero antes vamos a ver un poco de teoría:

El Timer0 es un temporizador/contador ascendente de 8 bits, cuando trabaja con el reloj del PIC
se le suele llama temporizador y cundo los pulsos los recibe de una fuente externa a través de la
patilla RA4/TOCKI se le llama contador, pero digamos que es el mismo perro con dos collares
diferentes. Para no liarnos con las patillas y el nombre de los registros voy a mostrar los registros
y patillas implicadas solo en el TMR0 utilizando el PIC16f877, aunque podría ser otro cualquiera
ya que este temporizador viene incorporado en todos los PIC.

RA4/TOCKI: cuando el temporizador trabaje como contador, los pulsos externos los recibirá a
través de esta patilla.

OSC1/CLKIN y OSC2/CLKOUT: son para conectar el oscilador que nos determinará a la


frecuencia que va a trabajar nuestro PIC, los dos tipos de osciladores más usados son el XT
(cristal de cuarzo) y el RC (resistencia y condensador).

Nota: cuando las patillas traen más de una función separadas por la barra del siete quiere decir
que según la configuración de los registros SFR que se muestran más abajo se utilizarán de una
manera o de otra.

OPTION

#RBPU INTED TOCS TOSE PSA PS2 IPS1 PS0


Bit 7 Bit 0

INTCON

GIE PEIE T0IE INTE RBIE T0IF INTF RBIF


Bit 7 Bit 0

Como podemos ver solo están marcados de un color diferente los que están implicados en el
funcionamiento del TMR0, vamos a ver la función que tienen:

TMR0: es un registro del que podemos leer el valor del contador en tiempo real, pero también
podemos escribir en él y alterar el valor de conteo.

OPTION: los bits que están en color verde son los que están implicados en la configuración del
TIMER0:

 PS0, PS1 y PS2: Configuración del preescaler. El preescaler es un divisor de pulsos que
está a la entrada del Timer-contador. El prescaler divide el número de pulsos que le
entran al timer-contador o al Wachtdog. El factor de división es el siguiente (según los
valores de PS2, PS1 y PS0 respectivamente

29
 PSA: Bit de asignación de prescaler. Si está a "1" el prescaler se asigna a WDT
(Wachtdog), si está a "0" se asigna al TMR0.
 TOSE: Bit de selección del tipo de flanco para el TMR0. A "1" se incrementa TMR0 por
flanco descendente de RA4, a "0" se incrementa TMR0 por flanco ascendente de RA4.
 TOCS: Selecciona la entrada de reloj de TMR0. A "1" la entrada de reloj de TMR0 es por
flanco de la patilla RA4, a "0" la entrada de reloj de TMR0 es por ciclo de reloj interno.

Pero el PIC también permite que se produzca una interrupción por desbordamiento del TMR0.

Cuando se produce una interrupción el programa abandona temporalmente lo que estaba


haciendo para atender la subrutina de interrupción, pero antes guarda en una región especial de la
memoria llamada pila la dirección de la siguiente instrucción de programa, para que cuando
acabe la subrutina de interrupción pueda seguir ejecutando el programa por donde se había
quedado.

El registro que configura todas las interrupciones es el INTCON, como veis está mapeado en los
dos bancos, una cosa muy útil cuando se trabaja en ensamblador para no tener que cambiar de
banco constantemente para su configuración, pero que en C no es necesario saber ya que es el
compilador el que se encarga de gestionar esto de una manera transparente para el programador.

Pues bien vamos a ver lo que significa cada uno de los bits que están implicados en la
interrupción por el TMR0:

 TOIF: solo se puede leer su valor, es un Flag o bandera que se pone a “1” cuando se
produce un desbordamiento del TMR0, (este o no este configurado para producir una
interrupción). Cuando se trabaja en Ensamblador, este bit hay que ponerlo a "0" por
programa, con CCS no tenemos que preocuparnos por ello. (je, je,.. otra cosa que nos
ahorramos)
 TOIE (Habilita la interrupción por desbordamiento de TMR0). Si este bit esta a "1" la
interrupción por desbordamiento de TMR0 es posible.
 GIE (Habilita las interrupciones globalmente). Este bit permite que cualquier interrupción
sea posible. Para poder usar cualquier interrupción hay que habilitarla globalmente e
individualmente.

Bueno como veis hay varias variables que podemos utilizar a nuestro antojo para utilizar este
recurso del PIC, la fórmula que las relaciona es la siguiente:

30
Y para ver cómo funciona todo esto con el compilador CCS vamos hacer el siguiente ejemplo:

Se trata de usar el Timer0 para generar una interrupción cada 32 ms utilizando el reloj interno del
microcontrolador. La función de interrupción se utilizará para hacer parpadear un Led en la
patilla RB7 del PIC.

Para ello vamos a utilizar en el circuito un cristal de cuarzo de 4 MHz.

Si nos fijamos en la formula anterior tenemos Interrupt_TMR0= 32 ms que es el valor que nos da
el ejemplo, también tenemos F.Oscilador = 4 MHz y nos quedan dos valores para tener resueltas
todas las variables de la ecuación el valor TMR0 que ya hemos dicho que se puede sobrescribir
su valor en cualquier momento durante la ejecución del programa y el Prescaler, pues le damos
el valor que queramos al Prescaler por ejemplo 256 y calculamos en la fórmula el valor que
tenemos que escribir en el registro TMR0 para obtener los 32 ms:

Si despejamos el valor del TMR0 nos sale un valor de 131 que en hexadecimal es 0x83.
Pues bien ya solo queda implementar todo esto en C, para ello en este ejemplo (y de paso vemos
una nueva funcionalidad que nos presenta este entorno de programación de PIC) vamos a utilizar
el Wizard o asistente para generar proyectos. Quiero aclarar primero que este asistente está
todavía en desarrollo por lo que es recomendable revisar el código que genera antes de compilar,
pues casi siempre tendremos que corregir algún error de sintaxis en el código generado, pero
suele ser una gran ayuda para el que empieza a trabajar con este compilador porque te genera un
código que es como una plantilla o esqueleto donde están ya incluidas las funciones para manejar
los recursos del PIC que hemos seleccionado con el asistente, para ello hacemos clic con el ratón
en el icono que pone PIC Wizard (si, si en el sombrero del mago), nos saldrá un cuadro de
dialogo de Windows para que guardemos nuestro proyecto donde queramos, una vez hecho esto
nos aparecerá la siguiente ventana:

En ella podemos ver que a la izquierda podemos seleccionar los diferentes recursos que tienen
los PIC y a la derecha tenemos dos pestañas: en Options configuramos el recurso seleccionado y

31
en la pestaña Code visualizamos el código que nos genera el asistente para ese recurso, para el
ejemplo que estamos haciendo en General, lo dejamos tal y como se muestra en la figura de
arriba. Si ahora hacemos clic en la pestaña code vemos el código generado por el asistente para
esta opción.

Inserted into .h file: no pertenece al código que se va a compilar, simplemente es una nota
informativa que nos está diciendo que el código que hay mas abajo se va a insertar en el archivo
de cabecera con extensión .h.

#FUSES NOWDT, XT, NOPROTECT: fuses cuya traducción literaria equivaldría a "fusibles" es
una directiva que establece unas configuraciones para el PIC que son necesarias a la hora de
grabar el programa en el, se pueden incluir aquí o configurarlas en el programa de grabación que
utilicemos, se guardan en la memoria de programa y según la versión de PIC que utilicemos
tendremos más o menos opciones, las que aquí aparecen tienen el significado siguiente:

NOWDT --> No se va a utilizar el Wachtdog.

XT --> Se va a utilizar un cristal de cuarzo para la base de tiempos del reloj del PIC.

NOPUT -->( Power Up Timer), cuando se configura como PUT al alimentar el circuito el
Microcontrolador espera un tiempo determinado para que se estabilicen las tensiones antes de
ejecutar el programa.

NOPROTECT --> El código grabado en el PIC no está protegido.

Hay que decir que cuando creamos un proyecto por medio del Wizard, este nos genera tres
archivos, que son los que se muestran en la figura de abajo:

32
Uno con extensión .c que es donde estará el grueso de nuestra aplicación, otro con extensión .h
que es donde estarán los archivos de cabecera y otro con extensión .pjt que es el proyecto en si y
que nos sirve para abrir y editarlo con el IDE del compilador.

Cuando compilemos y generemos nuestra aplicación aparecerán mas archivos pero estos que
hemos mencionado aquí son los que necesitamos para editar nuestro código fuente.
Sigamos, cuando seleccionemos la pestaña Communications debemos desmarcar la opción de
comunicación con el puerto serie RS232, ya que no la vamos a utilizar.

Configuración de los timmers:

En las opciones de este recurso desmarcamos la casilla Not used del Wachtdog por que no lo
vamos a utilizar, en el Timer 0 en Source seleccionamos Internal ya que vamos a utilizar los

33
pulsos de reloj del microcontrolador y en Resolution marcamos 256 us, que es el preescaler que
habíamos decidido utilizar. Vemos que las opciones del Timer 1 y el Timer 2 están
deshabilitadas, ya que PIC 16f84A solo tiene el temporizador timer 0.

Bien si ahora hacemos clic sobre la pestaña Code vemos el código generado por el asistente:

No hay gráfico

La primera línea como antes nos dice donde se va a insertar el código, en este caso en el archivo
.C y dentro de la función principal main(). La siguiente línea es una llamada a la función
setup_timer_0( parametro1|parametro2|…), que ya está declarada y definida dentro de las
librerías del compilador y es utilizada para configurar el registro OPTION del PIC, como vemos
acepta parámetros, con el primero RTCC_INTERNAL, le estamos diciendo que vamos a utilizar
el reloj interno, lo que viene después aunque en la figura no se vea muy bien es una barra vertical
“|” y es el símbolo que hay que utilizar para separar los diferentes parámetros que utilicemos, el
segundo parámetro que nos ha puesto es RTCC_DIV_1);); y como veis aparece doble el cierre
del paréntesis y el punto y coma, ¡¡error del asistente!!, lo que realmente tenemos que poner es
esto: RTCC_DIV_256, ya que este segundo argumento le pasa a la función el valor del
preescaler y queremos que sea 256. Bueno esto lo que me ha generado a mí, lo mismo a vosotros
os va bien. Vemos que no podemos corregir el código aquí, lo haremos después en los archivos
fuente que nos genera.

Por último nos queda configurar el registro INTCON, para ello seleccionamos Interrups y en
Options marcamos la casilla Timer0 overflow (usando como nombre de la interrupción
TIMER0).

El código generado será el siguiente:

34
Como vemos la rutina de interrupción va antes de la función main y consta de la directiva del
preprocesador #int_TIMER0, con esta directiva el programa sabe a qué dirección de programa
debe de ir cuando se dispare esta interrupción (podemos tener otras), y de la función de
interrupción propiamente dicha, donde tenemos que incluir nuestro código, veámoslo aquí
debajo de una forma más clara:

#int_TIMER0
Void TIMER0_isr(void)
{
//Nuestro código de interrupción.
}

Dentro de la función main incluye lo siguiente:

enable_interrupts(INT_TIMER0);
enable_interrupts(GLOBAL);

La primera instrucción habilita la interrupción particular del Timer 0 por desbordamiento y la


otra habilita las interrupciones globalmente, como dijimos al principio no hay que preocuparse
por resetear el flag de interrupción TOIF, ni de desactivar las interrupciones globales mientras se
esté dentro del código de interrupción por si se produce otra simultáneamente, como hay que
hacer cuando se trabaja en ensamblador, estas funciones lo hacen también pero de una manera
transparente al programador, por eso es un lenguaje de alto nivel.

Después de terminar con la configuración de los recursos pulsamos el botón de aceptar y el


asistente nos creará los archivos correspondientes:
Archivo de cabecera:

Archivo .C principal:

35
Pues bien este es el esqueleto de nuestra aplicación y sin escribir una línea de código. Ahora
sobre esta plantilla tenemos que escribir nuestro código para hacer nuestra aplicación funcional.

En el archivo de cabecera incluiremos la línea que está en negrita:

#include <16F877.h>
#use delay(clock=4000000)
#bit RB7=0x6.7 //definimos RB7 como el pin 7 de la puerta B

Con la directiva #bit le podemos asignar la dirección de memoria del bit de un registro a una
variable que podremos utilizar después para referirnos a ese bit.

En el archivo principal TMR0.C añadiremos también las sigientes líneas marcadas en negrita:

#include "C:\Prácticas PIC C\TMR0\TMR0.h"


#int_TIMER0

void TIMER0_isr(void)
{
if (RB7==0)
{
RB7=1;
set_TIMER0(0x83); //inicializa el timer0
}
else
{
RB7=0;
set_TIMER0(0x83); //inicializa el timer0
}
}

void main()
{
set_tris_b(0b01111111); //configura RB7 como salida el resto como entrada
RB7=0; //inicializo el bit RB7 a 0;
setup_timer_0(RTCC_INTERNAL|RTCC_DIV_256);
enable_interrupts(INT_TIMER0);
enable_interrupts(GLOBAL);

set_TIMER0(0x83); //inicializa el timer0

36
//el bucle infinito es necesario ya que si el micro entra en sleep
//desactiva la interrupción del TMR0
while(true);
}

Con la directiva #include lo que se hace es incluir un archivo dentro de otro, si queremos tener
todo el código fuente en un mismo archivo lo único que tenemos que hacer es sustituir esta línea
por el contenido del archivo de cabecera, pero es recomendable separar los archivos de cabecera
del resto del programa. C es un lenguaje estructurado y así es como trabaja, cuanto antes nos
acostumbremos a ello mejor.

¡ojo¡ si utilizamos los dos archivos y copiamos el código fuente y lo pegamos en un directorio
diferente tendremos que modificar la ruta actual al archivo de cabecera si no al compilar dará un
error de archivo no encontrado.

El funcionamiento del programa es muy simple, después de inicializar el puerto B inicializamos


el TMR0 por medio de la función set_TIMER0, el valor que recibe como parámetro esta función
es el valor que se escribirá en el TMR0 que es 0x86, valor que habíamos obtenido de la fórmula
anteriormente, como está activada la interrupción por desbordamiento del contador este empieza
a contar a partir de 0x86 ó 131 en decimal hasta 255 que equivale a 11111111 en binario como
es un contador de 8 bits al siguiente pulso de reloj se produce el desbordamiento y el contador de
programa va a la dirección de memoria donde está el vector de interrupción del TMRO y ejecuta
las instrucciones que están dentro de su función. La función de interrupción lo único que hace es
a través de una sentencia de control if–else encender o apagar el led, fijaros que en cada caso se
vuelve a cargar el valor del TMR0 para que el contador vuelva a empezar a contar por 131 y no
por cero, de esta manera la interrupción se producirá cada 32 ms que es lo que queríamos.

Bueno a hora toca construir nuestro circuito virtual y simularlo con Proteus para comprobar que
efectivamente es así.

Hemos agregado una sonda de tensión y para poder ver los valores que toma en el tiempo
incluimos una grafica en simulación digital. Si ampliamos la gráfica veremos que cada intervalo
tiene una duración de 32 ms. Como siempre el DSN y el código fuente os lo podéis bajar desde

37
la sección Descargas –> Apuntes -> Electrónica -> Microcontroladores -> Ejemplos prácticos
programación PIC en C.

Bueno yo creo que mas mascado no puede estar, para aquellos que no conozcan el lenguaje C y
les suene a chino lo que es una función y lo de pasar parámetros a una función. “No problem” en
el curso de teoría se verá detenidamente.

38
9. Uso del TMR1 como Temporizador
El Timer1 es un temporizador/contador ascendente parecido al TMR0, pero con algunas
peculiaridades que lo hacen muy interesante a la hora de incluir temporizaciones en nuestros
programas.

La primera de ellas, es que se trata de un contador de 16 bits cuyo valor se almacena en dos
registros de 8 bits el TMR1H y el TMR1L, ambos registros se pueden leer y escribir su valor
durante la ejecución del programa.

Cuando el Timer1 está habilitado, el valor de esos registros se incrementan desde 0000h a FFFFh
y una vez que llega a su máximo valor empieza otra vez desde 0 avisándonos de ello por medio
de la bandera TMR1F.

Si está activa la interrupción por desbordamiento del Timer 1 al desbordarse el contador, el


programa entra en la función de tratamiento a la interrupción por desbordamiento del Timer1.

El diagrama de bloques es el siguiente:

Algunas características de este Timer son las siguientes:

 El Timer1 puede funcionar con un oscilador externo y trabajar a una frecuencia distinta a
la del oscilador principal del PIC.
 Al igual que el TMR0 el Timer1 puede operar en dos modos: como temporizador y como
contador. El modo de funcionamiento está determinado por el tipo de reloj seleccionado
(interno -->temporizador, externo -->contador), lo configuramos por medio del bit
TMR1CS del registro TICON. Cuando está en modo contador su valor se incrementa en
cada flanco de subida de la señal de reloj externa.
 El tiempo que se tarda en incrementarse el contador se le suele llamar paso, el paso del
contador depende de la frecuencia del oscilador y del prescaler seleccionado.
 La fórmula para determinar los tiempos del Timer1 cuando es utilizado como temporizador
(Reloj interno) es la siguiente:

39
 El paso del contador vendrá determinado por:

Paso_Contador= 4/Frecuencia Oscilación.Prescaler

 El Timer1 se puede habilitar o deshabilitar por medio del bit TMR1ON del
registro T1CON.

Un dibujo con los bits de configuración del registro TICON, lo tienes en la figura de abajo:

Vamos a ver un ejemplo sencillo (como siempre) de cómo utilizar el Timer1 como temporizador
usando el reloj interno del micro. El ejemplo consiste simplemente en hacer parpadear un led a
un intervalo de 0.5 segundos, usando el Timer1 con la interrupción por desbordamiento
habilitada.

Para ello tenemos como siempre dos opciones, escribir nosotros todo el código o ayudarnos por
medio del asistente que trae CCS. Cualquiera de las opciones es válida, ya que el código es muy
sencillo.

El que utilice el asistente, deberá configurar primeramente el micro a utilizar y la frecuencia de


reloj. Yo voy a utilizar el PIC16f877 y un cristal de 4MHz (el viejo amigo de batallas
PIC16f84A no dispone de este recurso).

40
Después en el apartado timers habrá que configurar los parámetros para el Timer1, como vamos
a trabajar con el Timer1 en modo Temporizador, en el tipo de reloj seleccionaremos Internal,
después seleccionaremos una resolución entre las cuatro disponibles, según se muestra en la
figura de abajo.

La resolución es el tiempo que tarda el contador en incrementar su valor, es decir, el paso del
contador. Este valor, como hemos dicho ya, depende del Preescaler seleccionado y de la
frecuencia del reloj principal. Por ejemplo, si seleccionamos la última opción le estamos
41
diciendo al asistente que queremos un preescaler de 8 y como hemos seleccionado una
frecuencia de 4MHz para el reloj principal, el paso del contador será igual a:

4/Frecuencia Oscilación.Prescaler= 4/4MHz.8= 8u segundos.

El asistente también nos muestra el Overflow, que como hemos dicho también, es el tiempo que
tardará el contador en desbordarse. Suponiendo que carguemos el TMR1 con valor 0, que es
como viene por defecto, si aplicamos la formula obtendremos el valor de 524ms
(Desbordamiento_Timer1= 4/4MHz*8(65536-0)= 524 ms).

Como en vez de 524 ms, queremos obtener 500 ms. Sustituimos ese valor en la fórmula y
despejamos el valor a cargar en el TMR1:

El valor de TMR1 que sale es de 3036 que en hexadecimal es: 0x0BDC

Por último solo nos queda decirle al asistente que queremos utilizar la interrupción por
desbordamiento del Timer 1, según se muestra en la figura de abajo:

El código de nuestro ejemplo quedará de la siguiente forma:

01.#include <16F887.h>//Pic utilizado


02.//Palabra de configuración de los fusibles.
03.#FUSES NOWDT, XT, NOPUT, NOMCLR, NOPROTECT, NOCPD, NOBROWNOUT, NOIESO, NOFCMEN,
NOLVP, NODEBUG, NOWRT, BORV40
04.#use delay(clock=4000000)//Frecuencia del reloj principal
05.#bit RB7=0x06.7 //Identificador para el bit RB7
06.
07.#int_TIMER1
42
08.void TIMER1_isr(void)//Función de interrupción por desbordamiento TMR1
09.{
10. RB7=~RB7; //Togle RB7
11. set_timer1(0x0BDC);//carga del TMR1
12.}
13.
14.
15.
16.void main()
17.{
18. set_tris_b(0b01111111); //configura RB7 como salida el resto como entrada
19. RB7=0;//Inicializo RB7
20. setup_timer_1(T1_INTERNAL|T1_DIV_BY_8);//Setup timer: Reloj interno, preescaler= 8
21. enable_interrupts(INT_TIMER1);//Habilito interrupción particular del TIMER1
22. enable_interrupts(GLOBAL);//Habilito interrupciones globales
23. set_timer1(0x0BDC);//Carga del TMR1
24. while(true);//Bucle infinito hasta interrupción
25.
26.}

Siempre hay que limpiar un poco el código que nos genera el asistente, de parámetros y opciones
que están deshabilitadas y que no nos sirven para nada

El circuito a montar en Proteus, será el siguiente:

En el que se ha incluido una gráfica digital para medir el intervalo de tiempo de parpadeo del
diodo LED.

Si hacemos un zoom sobre la gráfica y añadimos un segundo cursor (Ctrl+c), podemos ver que el
intervalo es de 500 ms.

43
Comentario del programa:

El programa de lo sencillo que es, está más que explicado con los comentarios hechos en el
código fuente. Solo comentar las funciones que incluye CCS para el control del Timer1:

 set_timer1(value); //Función para inicializar los registros TMR1H y TMR1L.


 value=get_timer1; //Función para leer el valor del timer1
 setup_timer_1 (parámetros); //configuración del Timer1 por medio de parámetros. Los
parámetros pueden ser:
o T1_DISABLED: deshabilita el Timer1, esto me permite un ahorro de energía en el
PIC si no utilizo este recurso.
o T1_INTERNAL: fuente de reloj, el principal. Modo temporizador.
o T1_EXTERNAL: fuente de reloj externa. El timer funcionara como contador y como
condición para empezar el contaje debe de producirse un flanco de bajada en la señal
de reloj, mira la figura de abajo:

Este pequeño detalle que parece poco importante, no lo es en el caso de que queramos contar de
manera precisa los pulsos de entrada.

 T1_EXTERNAL_SYNC: con esto le digo al compilador que la señal de reloj externa se


sincronice con la señal de reloj principal. Como se ve en el diagrama de bloques la
44
sincronización se produce después del preescaler, por lo que hasta después del preescaler la
señal sigue siendo asíncrona. Hay que tener en cuenta que cuando tenemos definido este
parámetro si el PIC entra en modo SLEEP el TIMER1 no se incrementará aunque este
presente la señal de reloj externa.
 T1_CLK_OUT: con este parámetro pongo a 1 el bit T1OSCEN, para habilitar el oscilador
del Timer1. Cuando tengo seleccionada la fuente de reloj externa y este parámetro a la vez,
me permite independizar el timer1 del oscilador principal, por lo que el timer1 seguirá
contando aunque el PIC entre en modo SLEEP. La entrada de la señal de reloj será por la
patilla RC0/T1OSO/TICK1 del PIC. Cuando este parámetro no está definido T1OSCEN
toma el valor por defecto que es 0, en este caso y siempre y cuando este definido el
parámetro T1_EXTERNAL la entrada de la señal de reloj externa será por
RC1/TlOSI/CCP2. Este modo se utiliza para poder producir un reinicio del Timer1 a partir
de cualquiera de los módulos CCP1 o CCP2 del PIC.
 T1_DIV_BY_1, T1_DIV_BY_2, T1_DIV_BY_4, T1_DIV_BY_8: constantes para
seleccionar el preescaler que queremos seleccionar.

Los parámetros seleccionados se colocan juntos separados por el símbolo |.

Ejemplos:

setup_timer_1 ( T1_DISABLED );
setup_timer_1 ( T1_INTERNAL | T1_DIV_BY_4 );
setup_timer_1 ( T1_INTERNAL | T1_DIV_BY_8 );

Fuentes de información:

 Data Sheet PIC16F87X


 Manual CCS compiler

En el próximo artículo (sobre CCS), veremos cómo utilizar el timer1 como contador, utilizando
un reloj externo de 32.768Hz de frecuencia, y veremos si la precisión obtenida es suficiente para
implementar un reloj en tiempo real (RTC) de Horas, minutos y segundos.

El DSN de Proteus lo tienes aquí

#include <16F887.h>//Pic utilizado

//Palabra de configuración de los fusibles.


#FUSES NOWDT, XT, NOPUT, NOMCLR, NOPROTECT, NOCPD, NOBROWNOUT, NOIESO, NOFCMEN, NOLVP,
NODEBUG, NOWRT, BORV40
#use delay(clock=4000000)//Frecuencia del reloj principal
#bit RB7=0x06.7 //Identificador para el bit RB7

#int_TIMER1
void TIMER1_isr(void)//Función de interrupción por desbordamiento TMR1
{
RB7=~RB7; //Togle RB7
set_timer1(0x0BDC);//carga del TMR1
}

void main()
{
set_tris_b(0b01111111); //configura RB7 como salida el resto como entrada
RB7=0;//Inicializo RB7
setup_timer_1(T1_INTERNAL|T1_DIV_BY_8);//Setup timer: Reloj interno, preescaler=
8
45
enable_interrupts(INT_TIMER1);//Habilito interrupción particular del TIMER1
enable_interrupts(GLOBAL);//Habilito interrupciones globales
set_timer1(0x0BDC);//Carga del TMR1
while(true);//Bucle infinito hasta interrupción
}

46
10. Uso de la memoria EEPROM interna
CCS implementa muchas funciones para trabajar con las memorias EEPROM, algunas de ellas
son:

 value = read_eeprom (address): función básica para leer el valor de la EEPROM


interna del PIC. Devuelve un valor entero (int8) de un byte. "address" puede ser un entero
de 8 ó 16 bit. Dependiendo del PIC que utilicemos dispondremos de más ó menos
memoria EEPROM, por ejemplo el PIC 16f84A dispone de 64 bytes y los pic16F87X
tienen 256 bytes que se direccionan del 0 a 255.
 write_eeprom (address, value): esta función escribe un dato (entero de 8 bit) en la
dirección especificada en address en la memoria interna del PIC. Al igual que
read_eeprom address puede ser un entero de 8 ó 16 bit.

Algunos dispositivos permiten leer y escribir datos en la memoria de programa en tiempo de


ejecución, para los dispositivos que soportan esta funcionalidad CCS, nos proporciona las
siguientes funciones:

 value = read_program_eeprom (address): esta función lee un dato de la memoria de


programa del PIC y devuelve el valor leído como un entero de 16 bits. Adrress es un
entero de 16 ó 32 bits que depende del dispositivo empleado.
 write_program_eeprom (address, data): función homologa a la anterior pero que nos
permite escribir datos en la memoria de programa. data tiene que ser un entero de 16 bits.

CCS también incorpora funciones para leer y escribir en memorias EEPROM externas:

read_external_memory(address, dataptr, count )


write_program_memory( address, dataptr, count)

Como se ve CCS nos aporta una serie de funciones para trabajar fácilmente con este tipo de
memorias, estas que he puesto aquí son algunas de ellas, pero todavía hay más.

Algunas consideraciones a tener en cuenta sobre las memorias EEPROM es que son rápidas en el
proceso de lectura, pero pueden tardar varios ms en realizar un proceso de escritura. Otro factor
a tener en cuenta es que se pueden hacer operaciones de lectura sobre el valor de sus registros el
número de veces que se quiera, pero soportan un número limitado de ciclos de escritura /
borrado. Ese número según Microchip es de aproximadamente un millón.

La memoria EEPROM de datos no está mapeada en la zona de la memoria de datos donde se


ubican los registros SFR y GPR, si programáramos en Ensamblador deberíamos realizar una
serie de pasos para su lectura y escritura que aunque no difíciles resultan al menos laboriosos,
CCS nos permite abstraernos por completo del proceso de lectura y escritura, lo único que
tenemos que saber es las funciones que tenemos que aplicar y los parámetros y valores que
devuelven dichas funciones.

Como ejemplo del uso de este tipo de memorias vamos a utilizar las funciones básicas para leer y
escribir datos en la memoria interna del PIC: read_eeprom (address) y write_eeprom (address,
value).

47
El ejemplo es un contador del tipo "su turno" que solemos encontrar en algunos establecimientos
como carnicerías y pescaderías. Como funcionalidades mínimas se ha puesto que sea capaz de
contar del 0 al 99.

Y por supuesto que si se va la corriente guarde en memoria el último valor.

El esquema del circuito será el siguiente:

Cuando queremos utilizar más de un display y minimizar el número de patillas en el PIC para su
control hay varias formas de hacerlo, a continuación se muestran:

1. Una de ellas (la que se ha utilizado en este ejemplo) es utilizar un decodificador BCD a 7
segmentos como el 7447.

2. Otra forma es multiplexar las líneas de datos, es decir en cada instante solo habrá un display
activo pero el cambio de uno a otro será tan rápido que para el ojo humano parecerá que los
dos están activos a la vez, este sistema es bueno porque nos ahorramos los decodificadores,
pero si utilizamos más de cuatro display, notaremos un parpadeo molesto.

El código del programa será el siguiente:

01./*-----------------------------------------------------------*\
02.| Uso de la memoria EEPROM del PIC |
03.| |
04.| |
05.\*-----------------------------------------------------------*/
06.
07.#include <16F877.h>
08.//Configuración de los fusibles.
09.#FUSES NOWDT, XT, NOPUT, NOPROTECT, NOBROWNOUT, NOLVP, NOCPD, NOWRT, NODEBUG
10.#use delay(clock=4000000) //Frecuencia de reloj 4MHz
11.#byte puerto_D = 0x08 // Identificador para el puerto C.
12.
13. #int_EXT
14.
15. void EXT_isr( void )

48
16. {
17.
18. if ((read_eeprom(0)==0x99)||(read_eeprom(0)==0xFF))
19. {
20. write_eeprom(0,0);
21. puerto_D=read_eeprom(0);
22. }
23. else if ((read_eeprom(0) & 0x0F)<0x09)
24. {
25. write_eeprom(0,(read_eeprom(0))+1);
26. puerto_D=read_eeprom(0);
27. }
28. else if ((read_eeprom(0) & 0x0F)>=0x09)
29. {
30. write_eeprom(0,(read_eeprom(0))+7);
31. puerto_D=read_eeprom(0);
32. }
33.
34. }
35.
36. void main()
37. {
38. set_tris_b(0xFF); //Puerto B como entrada
39. set_tris_d(0x00);//Puerto D como salida
40. enable_interrupts(GLOBAL); // Se habilita la interrupción global
41. enable_interrupts(INT_EXT); // Se habilita la interrupción externa
42. puerto_D =0xFF; //inicializo puerto D
43. //write_eeprom(0,0xFF);Resetear EEPROM
44. while(true)
45. {
46. //Resto del programa
47.
48. }
49. }

Comentario del programa:

El ejemplo básicamente es un contador ascendente de 0 a 99 que incrementa su valor cada vez


que pulsamos el pulsador "su turno" para evitar que el contador se reinicie y empiece a contar
desde cero cada vez que se vaya la corriente, el valor actual del contador se almacenará en la
memoria EEPROM del PIC en vez de en la memoria RAM. Como solo queremos guardar un
valor que estará comprendido entre 0 y 99, solo utilizaremos el primer byte de la memoria
EEPROM. Para detectar cuando se pulsa el pulsador, se utiliza la interrupción externa en la
patilla RB0/INT.

Dentro de la función de interrupción nos encontramos con tres sentencias condicionales:

if ((read_eeprom(0)==0x99)||(read_eeprom(0)==0xFF))
{
write_eeprom(0,0);//escribo el valor 0 en la dirección 0 de la memoria EEPROM
puerto_D=read_eeprom(0);//asigno al puerto D el valor devuelto por la función de
lectura de la EEPROM
}

La primera vez que se ejecute el programa el valor de la EEPROM es 0xFF (viene así de fábrica)
por lo que tendremos que sobre escribir su valor a 0 para iniciar el contaje, después de esto esta
condición solo se cumplirá cuando el contador llegue a 99.

else if ((read_eeprom(0) & 0x0F)<0x09)


{
write_eeprom(0,(read_eeprom(0))+1);
puerto_D=read_eeprom(0);
49
}

Cada "nibble" del puerto D está conectado a un decodificador por lo que tenemos RD0-RD3 al
decodificador U2 y su salida conectado al display de las unidades a través del bus de datos (hay
que hacer un zoom sobre el esquema en Proteus para ver las conexiones) y RD4-RD7 al
decodificador U3, que está conectado al display de las decenas.

Pues bien, si tenemos 4 bits por "nibble" tendremos 16 combinaciones posibles, incrementamos
con 1 el valor de la EEPROM mientras el valor del "nibble" de menor peso sea < 9. Fijaros como
la función de escritura en la EEPROM nos permite poner como parámetro la función de lectura
por lo que no es necesario declarar una variable local para hacer de intercambio.

Una vez incrementado el valor de la EEPROM, hacemos una nueva lectura y asignamos su valor
al puerto D.

else if ((read_eeprom(0) & 0x0F)>=0x09)


{
write_eeprom(0,(read_eeprom(0))+7);
puerto_D=read_eeprom(0);
}

Los valores del "nibble" de menor peso correspondientes al rango 10-15 los saltamos sumando 7
en vez de uno. El nibble de más peso de la lectura de la EEPROM no me interesa ya que el
primer if ya se encarga de ponerlo a cero cuando el valor llegue a 0x99. Para discriminar el
"nibble" de más peso de la EEPROM lo enmascaramos antes de hacer la comparación

Podemos ver los registros de la EEPROM interna del PIC, si en Proteus seleccionamos Debug--
>PIC CPU EPROM Memory.

50
Proteus simula bastante bien el uso de la memoria EEPROM del PIC, si paramos la simulación y
volvemos a iniciarla vemos como los valores de la EEPROM se mantienen.

El código del programa lo tienes aquí

/******************************************************************************
* *
* DESCRIPCIÓN: Uso de la memoria EEPROM del PIC *
* EEPROM.c *
* *
******************************************************************************/

#include <16F877.h>

//Configuración de los fusibles.


#FUSES NOWDT, XT, NOPUT, NOPROTECT, NOBROWNOUT, NOLVP, NOCPD, NOWRT, NODEBUG

#use delay(clock=4000000) //Frecuencia de reloj 4MHz

#byte puerto_D = 0x08 // Identificador para el puerto C.

#int_EXT
void EXT_isr( void )
{
if ((read_eeprom(0)==0x99)||(read_eeprom(0)==0xFF))
{
write_eeprom(0,0);
puerto_D=read_eeprom(0);
}
else if ((read_eeprom(0) & 0x0F)<0x09)
{
write_eeprom(0,(read_eeprom(0))+1);
51
puerto_D=read_eeprom(0);
}
else if ((read_eeprom(0) & 0x0F)>=0x09)
{
write_eeprom(0,(read_eeprom(0))+7);
puerto_D=read_eeprom(0);
}
}

void main()
{
set_tris_b(0xFF); //Puerto B como entrada
set_tris_d(0x00); //Puerto D como salida

enable_interrupts(GLOBAL); //Se habilita la interrupción global


enable_interrupts(INT_EXT); //Se habilita la interrupción externa

puerto_D =0xFF; //inicializo puerto D

//write_eeprom(0,0xFF);Resetear EEPROM
while(true)
{
//Resto del programa
}
}

52
11. Control de varias interrupciones
Para controlar varias interrupciones simultáneamente se va a utilizar la interrupción sobre la
cuenta del registro TMR0 y la interrupción al cambio de nivel de las líneas RB4 a RB7.

La aplicación establece la ejecución de las tres siguientes tareas a una velocidad tal que se
asemeja con una ejecución paralela:

 El LED D1 es intermitente con un tiempo de 1 seg.

 El LED D2 se prende por tres ciclos del LED D1 cuando se presiona una tecla cualquiera de
las líneas RB4 a RB7.

 El LED D3 es intermitente a una frecuencia correspondiente a la frecuencias de oscilación


del cristal dividido para 4 y una relación 1:256, el tiempo de prendido y apagado se establece
con la cuenta de TMR0 hasta producirse overflow.

El circuito a ser realizado se presenta a continuación:

El código de este ejemplo en Assembler se encuentra en el capítulo de “Manejo de


Interrupciones”, DBLINT.ASM, y en lenguaje C se muestra a continuación en DblInt.c:

/******************************************************************************
* *
* DESCRIPCIÓN: Control de varias interrupciones *
* DblInt.c *
* *
******************************************************************************/

#include <16f877a.h>

#use delay(clock=4000000)
#fuses XT,NOWDT,NOPUT,NOPROTECT,NOLVP
53
#use fixed_io(b_outputs= PIN_B1, PIN_B2, PIN_B3)

#define LED1 0
#define LED2 1
#define LED3 2

#byte PORTB = 0x06

int nTick;

#int_RTCC
RTCC_isr() //realiza cambio de estado de D3 en cada overflow del TMR0
{
if (bit_test(PORTB, LED3))
output_low(PIN_B3);
else
output_high(PIN_B3);
}

#int_RB //pone en 1 el led D2 al cambiar de estado RB4-RB7


RB_isr()
{
output_high(PIN_B2);
nTick = 6;
}

void main()
{

set_tris_b(0xF0);
setup_counters (RTCC_INTERNAL, RTCC_DIV_256);

enable_interrupts(INT_RTCC);
enable_interrupts(INT_RB);
enable_interrupts(GLOBAL);

while(TRUE) {
delay_ms(1000); //Cambio de estado del led D1

if (bit_test(PORTB, LED1))
output_low(PIN_B1);
else
output_high(PIN_B1);

if (bit_test(PORTB, LED2)) {
nTick -= 1;
if ( nTick == 0)
output_low(PIN_B2);
}

Análisis del código DBLINT.C

El control de interrupción verifica el evento generado por una interrupción, por lo cual, no es
necesario realizar una verificación para determinar qué evento fue generado, por lo que se
ejecuta la función correspondiente. Es nuestro caso sobre flujo del Timer0 y cambio de estado de
las patitas RB3, … RB0, respectivamente:

#int_RTCC
RTCC_isr()

54
#int_RB
RB_isr()

55
12. Ejemplo comunicación serie I2C entre un PIC y la memoria
EEPROM 24LC256A
En este ejemplo voy a mostrar lo fácil que es leer y grabar datos en una memoria EEPROM
Externa desde un PIC utilizando el lenguaje C y el compilador CCS, concretamente la memoria a
utilizar será la 24LC256A fabricada por Microchip.

Para empezar vamos a describir algunas características del nuevo componente que vamos a
utilizar y cuyo patillaje tienes en la figura de abajo.

Esta es una memoria que tiene un tamaño de 32 Kbytes, su estructura está organizada en palabras
de 1 byte (8 bits) de longitud, por lo tanto dispondremos en total de 32x8=256 kbits para
almacenar información.

La característica principal de esta memoria es que implementa el interfaz I2C para su


comunicación serie con otros dispositivos electrónicos.

El protocolo I2C fue desarrollado por la empresa Philips, ahora llamada NXP, hace ya bastante
tiempo impulsado por la necesidad en el mercado de establecer una interfaz que comunicara
entre si varios dispositivos electrónicos y que ahorrara el máximo número de patillas en los
componentes, hay mucha información en la red sobre este protocolo por lo que solo voy a citar a
modo de resumen las características que considero más relevantes acerca de este protocolo. Al
final del artículo pondré los enlaces a varias fuentes para el que quiera profundizar en este
protocolo de comunicación.

Características básicas del protocolo I2C

 El bus de comunicación utiliza dos cables. Uno para transmitir los datos (SDA) y otro
para la señal de reloj (SCL). Ambas líneas se tienen que conectar a la alimentación
(Vcc) a través de resistencias Pull-UP (en el esquema del ejemplo se muestra el
conexionado). Además la masa de los componentes que se inter conexionan entre si
debe de ser común.
 El protocolo de comunicación es del tipo Maestro-Esclavo, aunque el protocolo permite
que haya varios maestros, lo normal es que solo haya uno, el dispositivo que hace de
maestro es el que gobierna la comunicación y es el que controla la señal de reloj (SCL).
Los dispositivos que hacen de esclavos responden a las peticiones del maestro.
 Cada dispositivo conectado al bus se identifica por una única dirección, el protocolo
admite utilizar 7 ó 10 bits para el direccionamiento, aunque lo normal es que sean siete.

56
Los cuatro primeros bits están asignados al fabricante del dispositivo y los tres últimos son
configurables por hardware. En el caso de la memoria que estamos viendo el código del
fabricante es: 1010, como tenemos tres bits configurables podremos conectar entre si hasta ocho
memorias 24LC256 juntas.

La trama de un mensaje simple donde el dispositivo que hace de maestro envía datos para ser
escritos en la EEPROM sería el siguiente:

 Se envía el bit de inicio (S)


 Se envía la dirección del esclavo, en este caso la memoria EEPROM
 Se envía un bit más junto con la dirección del esclavo (R/W) para indicar que lo que se
quiere es leer o escribir. Si este bit es cero el proceso será de escritura.
 Después el maestro manda un pulso de la señal de reloj, durante el cual el esclavo debe
contestar con un ACK (señal de reconocimiento), si el maestro no recibe esta señal
interrumpe la comunicación.
 Después se envían los bytes de dirección necesarios para identificar el registro de la
EEPROM donde queremos realizar el proceso de escritura.
 Mandamos el dato que se almacenará en la EEPROM
 Y por último se manda el bit de parada para finalizar la comunicación.

¿Como se gestiona todo esto en C y con el compilador CCS?

CCS dispone de librerías específicas para gestionar la comunicación I2C de determinados


componentes, entre ellos algunas memorias EEPROM. Basta con incluir la librería
correspondiente por medio de la directiva #include y utilizar sin más las funciones de la librería.
Pero si el componente que queremos utilizar no dispone del driver pertinente en CCS, tenemos
otra opción, que es desarrollar nuestro propio driver, para ello disponemos como ayuda de una
directiva homologa a #use rs232 (options), se trata de #use i2c (options).
57
Al incluir esta directiva el compilador nos permite utilizar las siguientes funciones para controlar
la comunicación serie I2C entre varios dispositivos:

 i2c_isr_state()
 i2c_poll()
 i2c_read()
 i2c_slaveaddr()
 i2c_start()
 i2c_stop()
 i2c_write()

Además los pics que soportan este tipo de comunicación disponen de modulos y registros
específicos para facilitar la comunicación I2C por hardware, lo mismo que teníamos con la
comunicación serie RS232.

Como veis esto da pie a muchos ejemplos, ya que hay bastantes dispositivos (Relojes en tiempo
real, convertidores A/D, expansión de puertos de E/S, LCDs, etc) que implementan esta interfaz,
también tenemos la posibilidad de comunicar varios PICs entre sí, uno haciendo de maestro y los
otros de esclavos.

Pero vamos poco a poco y para empezar vamos hacer el ejemplo del contador "su turno" que
ya teníamos hecho, pero en vez de guardar los datos en la memoria EEPROM interna del PIC lo
vamos hacer en la memoria externa 24LC256.

El esquema modificado en Proteus será el siguiente:

58
Como veis en la imagen de arriba el circuito es el mismo que habíamos hecho para el ejemplo
del uso de la memoria EEPROM interna del PIC incluyendo la memoria 24LC256. Las patillas
A0, A1 y A2 de la memoria están a masa, por lo que la dirección física del componente en la red
será: 1010000. Los pines SDA y SCL son los que se conectan al bus IC2, a través de las
resistencias pullup.

Puede que alguien se pregunte que valor hay que poner a estas resistencias (pullup) en un
circuito real, pues la verdad es que no existe un valor ideal para todos los casos, dependerá de
factores como la velocidad a la que hagamos trabajar el bus (100-400 kbps) y de la carga
capacitiva que tenga el circuito (max. 400 pF), un valor que se aconseja como estándar es de
4k7. Pero para que el circuito simule bien en Proteus se deben poner las genéricas pullup.

El pin 7 de la memoria (WP) es para proteger ó no los datos guardados en la memoria, si está a
masa los datos no están protegidos y se puede escribir o sobreescribir los datos ya guardados. Si
está a uno (Vcc), no es posible escribir en la EEPROM.

Otro componente de Proteus incluido en el circuito es el I2C Debugger con el cual podemos ver
la trama de datos en tiempo de ejecución. En la figura de abajo se detallan los bits de la trama en
un proceso de escritura en la EEPROM.

El código fuente del Microcontrolador es el siguiente:

01./*-----------------------------------------------------------------------*\
02.|Ejemplo comunicación serie I2C entre un PIC y la memoria EEPROM 24LC256A |
03.| |
04.| |
05.\*------------------------------------------------------------------------*/
06.
07.#include <16F877.h>
59
08.//Configuración de los fusibles.
09.#FUSES NOWDT, XT, NOPUT, NOPROTECT, NOBROWNOUT, NOLVP, NOCPD,
NOWRT, NODEBUG
10.#use delay(clock=4000000) //Frecuencia de reloj 4MHz
11.#byte puerto_D = 0x08 // Identificador para el puerto C.
12.#include "24256.c" //Incluimos librería 24256
13.
14. #int_EXT
15.
16. void EXT_isr( void )
17. {
18.
19. if ((read_ext_eeprom(0)==0x99)||(read_ext_eeprom(0)==0xFF))
20. {
21. write_ext_eeprom(0,0);
22. puerto_D=read_ext_eeprom(0);
23. }
24. else if ((read_ext_eeprom(0) & 0x0F)<0x09)
25. {
26. write_ext_eeprom(0,(read_ext_eeprom(0))+1);
27. puerto_D=read_ext_eeprom(0);
28. }
29. else if ((read_ext_eeprom(0) & 0x0F)>=0x09)
30. {
31. write_ext_eeprom(0,(read_ext_eeprom(0))+7);
32. puerto_D=read_ext_eeprom(0);
33. }
34.
35. }
36.
37. void main()
38. {
39. set_tris_b(0xFF); //Puerto B como entrada
40. set_tris_d(0x00);//Puerto D como salida
41. enable_interrupts(GLOBAL); // Se habilita la interrupción global
42. enable_interrupts(INT_EXT); // Se habilita la interrupción externa
43. puerto_D =0xFF; //inicializo puerto D
44.
45. init_ext_eeprom();//inicializamos memoria EEPROM externa
46. //write_ext_eeprom(0,0xFF);//Resetear registro 00 EEPROM
47.
48. while(true)
49. {
50. //Resto del programa
51.
52. }
53. }

Comentario del programa

60
Como he dicho anteriormente en este primer ejemplo del uso del protocolo serie I2C vamos a
utilizar el driver que nos facilita CCS. Para ello simplemente lo incluimos en nuestro programa
por medio de la directiva #include:

#include "24256.c" //Incluimos librería 24256

Nota: por defecto la librería define los pines RB1 y RB0 como SDA y SCL, como en nuestro
ejemplo RB0 ya está ocupado. Hay que modificar esos valores en la librería y sustituirlos por
RC4 y RC3, de la siguiente manera.

Donde pone:

#define EEPROM_SDA PIN_B1


#define EEPROM_SCL PIN_B0

poner:

#define EEPROM_SDA PIN_C4


#define EEPROM_SCL PIN_C3

Las funciones que incluye esta librería son:

 init_ext_eeprom(); //llamada a la función de inicialización. Debe de ir antes que las dos


de abajo
 write_ext_eeprom(a, d); //Escribe el byte d en la dirección a
 d = read_ext_eeprom(a); // lee el valor que hay en la dirección a y se lo asigna a la
variable d

Pues bien la única modificación que hay que hacerle al código, con respecto al que habíamos
hecho para grabar los datos en la EEPROM interna del PIC es sustituir estas funciones, según se
muestra en el código fuente, fácil no!!.

Proteus nos permite también ver el estado de los registros de está memoria en el proceso de
simulación.

61
El código fuente está aquí:

/*-----------------------------------------------------------------------*\
|Ejemplo comunicación serie I2C entre un PIC y la memoria EEPROM 24LC256A |
| |
| |
\*------------------------------------------------------------------------*/

#include <16F877.h>

//Configuración de los fusibles.


#FUSES NOWDT, XT, NOPUT, NOPROTECT, NOBROWNOUT, NOLVP, NOCPD, NOWRT, NODEBUG
#use delay(clock=4000000) //Frecuencia de reloj 4MHz
#byte puerto_D = 0x08 // Identificador para el puerto C.
#include "24256.c" //Incluimos librería 24256

#int_EXT

void EXT_isr( void )


{
if ((read_ext_eeprom(0)==0x99)||(read_ext_eeprom(0)==0xFF))
{
write_ext_eeprom(0,0);
puerto_D=read_ext_eeprom(0);
}
else if ((read_ext_eeprom(0) & 0x0F)<0x09)
{
write_ext_eeprom(0,(read_ext_eeprom(0))+1);
puerto_D=read_ext_eeprom(0);
}
else if ((read_ext_eeprom(0) & 0x0F)>=0x09)
{
write_ext_eeprom(0,(read_ext_eeprom(0))+7);
62
puerto_D=read_ext_eeprom(0);
}
}

void main()
{
set_tris_b(0xFF); //Puerto B como entrada
set_tris_d(0x00);//Puerto D como salida
enable_interrupts(GLOBAL); // Se habilita la interrupción global
enable_interrupts(INT_EXT); // Se habilita la interrupción externa
puerto_D =0xFF; //inicializo puerto D

init_ext_eeprom();//inicializamos memoria EEPROM externa


//write_ext_eeprom(0,0xFF);//Resetear registro 00 EEPROM

while(true)
{
//Resto del programa

}
}

63
13. Comunicación serie entre dos PICs con la USART
En este ejemplo vamos a ver como comunicar dos PIC’s entre sí vía serie RS-232 haciendo uso
del modulo USART (Universal Synchronous/Asynchronous Receiver Transmiter.), que
incorporan la mayoría de los PIC’s de la gama media/alta. Con este modulo hardware se puede
implementar una comunicación serie del tipo síncrona ó asíncrona.

 Síncrona: necesita una conexión adicional para la señal de reloj. Una Usart hace de
Master y la otra de esclava. La comunicación es del tipo halfduplex (bidireccional por
turnos). Se emplea cuando se quiere comunicar un PIC con otro dispositivo electrónico,
como una memoria EEPROM externa.
 Asíncrona: no se necesita una conexión para la señal de reloj, los relojes del transmisor y
del receptor son independientes, aunque deben de tener la misma frecuencia, la
sincronización entre ambos se hace añadiendo unos bits adicionales (bit de inicio y bit de
parada) al byte de datos, que puede estar formado por 8 ó 9 bits. La comunicación puede
llegar a ser hasta dúplex completo (bidireccional simultanea). Este tipo de conexión es la
que se utiliza normalmente para comunicar un PIC con un Ordenador ó para comunicar
dos PIC’S entre sí, es la más usada y es la que vamos a ver a continuación como ejemplo.

CCS gestiona una comunicación serie a través de la directiva

#users232 (lista de parámetros)

Esta directiva se puede poner en cualquier parte del programa pero siempre después de:

#usedelay(Frecuencia)

Si nuestro PIC no tiene USART como es el caso del PIC 16f84A, CCS nos implementa la
comunicación serie por software y de forma totalmente transparente para el programador. Sin
embargo el utilizar un PIC con USART nos permite disponer de una serie de funcionalidades
adicionales, como el empleo de interrupciones para controlar el envío y recepción de datos serie.

Si queremos realizar una comunicación serie utilizando la USART del PIC, las conexiones
tendrán que ser a la fuerza las siguientes: RC6/TX/CK para la transmisión de datos y
RC7/RX/DT para la recepción de datos, según se muestra en la figura de abajo.

64
Los registros del PIC implicados en este recurso son los siguientes:

 PIR1–> Registro para señalizaciones ó banderas.


 RCSTA–> Registro para el control de la parte receptora de la USART
 TXREG–> Registro de datos de transmisión
 SPBRG–> Registro para el control del generador de frecuencia, encargado de generar la
frecuencia en baudios para realizar la transmisión.
 TXSTA–> Registro para el control de la parte de transmisión de la USART
 PIE1–> Habilitación de interrupciones
 RCREG--> Registro de datos de recepción

El circuito del ejemplo será es el siguiente:


65
El PIC U1 hará de PIC transmisor de datos, se encargará de comprobar si se pulsa alguna tecla
en el teclado y en el caso de que así sea, mandará el valor del carácter (1 byte) de la tecla
pulsada, correspondiente al código del teclado. El segundo PIC implementará en su código una
interrupción por recepción de datos serie en la USART, recibirá el carácter procedente de U1 y
visualizará su valor en un display de siete segmentos de cátodo común.

Código del programa

Al haber dos PIC’s tendremos que hacer dos programas independientes, uno para el transmisor y
otro para el receptor.

Transmisor:
01.
02./*--------------------------------------------------------*\
03.| Comunicación entre dos PIC's por USART. Transmisor |
04.| |
05.| |
06.\*--------------------------------------------------------*/
07.
08.#include <16F877.h>
09.#FUSES NOWDT, XT, NOPUT, NOPROTECT, NOBROWNOUT, NOLVP, NOCPD, NOWRT, NODEBUG
10.#use delay (clock = 4000000)
11.#include "kbd_2.c"
12.#use rs232(uart1, baud=9600)//usart1 -->ajuste de XMIT y RCV para la USART 1
13.void main() {
14. char c;
15. kbd_init(); //inizializo drivers teclado
16. while(true)
17. {
18. c = kbd_getc();
19. if(c != 0 )//si se ha pulsado una tecla
20. {
21. putc(c);//Envía el caracter vía serie por la USART del PIC
22.

66
23. }
24.
25. }
26.
27.}

Comentario

En la parte del código del transmisor incluimos una librería nueva:

#include "kbd_2.c"

Esta librería es una modificación de KBDD.c que incluye CCS en la carpeta Drivers, creada en
la instalación del programa. La que viene por defecto controla un teclado de 4 (filas) X 3
(Columnas). La modificación que he hecho permite controlar un teclado de 4 X 4.

Viendo el código original es fácil hacer las modificaciones pertinentes para poder controlar un
teclado personalizado.

Solo son necesarias dos funciones para utilizar esta librería que son:

kbd_init(); //inicializa el drivers del teclado.

kbd_getc();//devuelve el código ASCII de la tecla pulsada, si no se pulsa ninguna


devuelve 0.

El uso de estas librerías tiene un problema y es que tienen copyright, si vemos el comentario en
el encabezado, nos encontramos con esto:

01.///////////////////////////////////////////////////////////////////////////
02.//// KBDD.C ////
03.//// Generic keypad scan driver ////
04.//// ////
05.//// kbd_init() Must be called before any other function. ////
06.//// ////
07.//// c = kbd_getc(c) Will return a key value if pressed or /0 if not ////
08.//// This function should be called frequently so as ////
09.//// not to miss a key press. ////
10.//// ////
11.///////////////////////////////////////////////////////////////////////////
12.//// (C) Copyright 1996,2003 Custom Computer Services ////
13.//// This source code may only be used by licensed users of the CCS C ////
14.//// compiler. This source code may only be distributed to other ////
15.//// licensed users of the CCS C compiler. No other use, reproduction ////
16.//// or distribution is permitted without written permission. ////
17.//// Derivative programs created using this software in object code ////
18.//// form are not restricted in any way. ////
19.///////////////////////////////////////////////////////////////////////////

Que nos viene a decir, entre otras cosas, que podemos crear programas derivados de este
software y distribuir el código objeto generado al compilar pero no se puede distribuir el código
fuente. Por lo que este es el motivo de no mostrar la modificación hecha sobre esta librería.

Para que os funcione el ejemplo al completo tendréis que modificar la librería para que sea capaz
de controlar el teclado 4X4 ó modificar el circuito para tener un teclado de 4X3.

Otra novedad de este ejemplo son los parámetros de configuración de la directiva:

67
#use rs232(uart1, baud=9600)//usart1 -->ajuste de XMIT y RCV para la USART 1

En el caso de utilizar la USART del pic, CCS nos permite configurar fácilmente el bit de
transmisión (XMIT) y el bit de recepción (RCV) dentro de la directiva #use rs232, el “1” es
porque hay PIC,s que tienen más de una USART.

Luego el programa entra en un bucle infinito para ir chequeando en cada momento si se ha


pulsado alguna tecla. Si detecta que se ha pulsado alguna tecla, envía el valor de la tecla pulsada
por el canal serie por medio de la finción putc().

Receptor:
01./*--------------------------------------------------------*\
02.| Comunicación entre dos PIC's por USART. Receptor |
03.| |
04.| |
05.\*--------------------------------------------------------*/
06.
07.#include <16F877.h>
08.#FUSES NOWDT, XT, NOPUT, NOPROTECT, NOBROWNOUT, NOLVP, NOCPD, NOWRT, NODEBUG
09.#use delay (clock = 4000000)
10.#byte puerto_D = 0x08
11.#use rs232(uart1, baud=9600)
12.char dato;
13.
14.#int_rda
15. void rd_isr(void)//función de interrupción por recepción de datos USART
16. {
17. dato= getc();
18.
19. switch(dato)//visualizo el caracter recibido en el display
20. {
21. case '0':
22. puerto_D = 0x3F;
23. break;
24. case '1':
25. puerto_D = 0x06;
26. break;
27. case '2':
28. puerto_D = 0x5B;
29. break;
30. case '3':
31. puerto_D = 0x4F;
32. break;
33. case '4':
34. puerto_D = 0x66;
35. break;
36. case '5':
37. puerto_D = 0x6D;
38. break;
39. case '6':
40. puerto_D = 0x7D;
41. break;
42. case '7':
43. puerto_D = 0x07;
44. break;
45. case '8':
46. puerto_D = 0x7F;
47. break;
48. case '9':
49. puerto_D = 0x6F;
50. break;
51. case 'A':
52. puerto_D = 0xF7;

68
53. break;
54. case 'B':
55. puerto_D = 0x7C;
56. break;
57. case 'C':
58. puerto_D = 0x58;
59. break;
60. case 'D':
61. puerto_D = 0x5E;
62. break;
63. case 'E':
64. puerto_D = 0x79;
65. break;
66. case 'F':
67. puerto_D = 0x71;
68. break;
69. default:
70.
71. break;
72. }
73. }
74.void main()
75. {
76.
77. enable_interrupts(global);//Habilito interrupción USART
78. enable_interrupts(int_rda);
79.
80. set_tris_d( 0x00 ); // Puerto D como salida.
81. puerto_D =0x00; //inicializo puerto D
82.
83. while(true){
84.
85. //Código principal
86. }
87.
88. }

Comentario

En la parte del código del receptor se utiliza el recurso de interrupción por recepción de datos de
la USART. Si utilizamos el asistente para crear el proyecto del receptor, tendremos que marcar la
opción que se muestra en la figura de abajo.

69
Cuando se produce la interrupción se obtiene el carácter recibido por medio de la función getc(),
después por medio de la sentencia switch se obtiene la salida para el display en el puerto D en
función del valor recibido.

Como siempre podemos ejecutar el programa paso a paso con Proteus y ver como CCS configura
los diferentes registros del PIC implicados en la comunicación serie.

En la figura de arriba se muestra por medio de la ventana Watch Windows como se pone a 1 el
bit RCIF (flag de interrupción por recepción de datos en la USART) del registro PIR1 en el
momento que se entra en la función de interrupción.

Nota: Realmente en este ejemplo la comunicación es unidireccional (simplex), ya que el PIC U2


no transmite datos solo recibe los datos procedente de U1, por lo que la conexión que une la
patilla de transmisión de U2 con la de recepción de U1 se podría suprimir en el circuito.
Aquí tienes un pequeño video del programa funcionando:

El código del circuito lo tienes aquí

/******************************************************************************
* *
* *
* DESCRIPCIÓN: Librería para el manejo de un teclado matricial 4x4. *
* *
******************************************************************************/

#byte port_B = 6
#bit ROW_3 = 0x06.7 // Identificadores de bits
#bit ROW_2 = 0x06.6
#bit ROW_1 = 0x06.5
#bit ROW_0 = 0x06.4

/* DEFINICIÓN DE CONSTANTES SIMBÓLICAS ****************************************/


#define COLUMN_0 1
#define COLUMN_1 2
#define COLUMN_2 4
70
#define COLUMN_3 8
#define NUMBER_COLUMNS 4
#define NUMBER_ROWS 4
#define DEBOUNCER_DELAY 30

/* DEFINICIÓN DE CONSTANTES ***************************************************/


char const matrixKeyBoard[ NUMBER_ROWS ][ NUMBER_COLUMNS ] =
{
/* Disposición del teclado */
'1', '2', '3', 'F',
'4', '5', '6', 'E',
'7', '8', '9', 'D',
'A', '0', 'B', 'C',
};

/* DEFINICIÓN DE VARIABLES LOCALES ********************************************/


static unsigned column;

/* FUNCIONES PÚBLICAS *********************************************************/


void mkd_int( void )
{
/* Inicialización general */
set_tris_b( 0xF0 ); /* <RB7:RB4> como entrada, <RB3:RB0> como salida */
port_B = 0;
column = 0;
}

char mkd_keyPress( void )


{
static char keyPress = '\0';

/* Se ha de esperar aprox. 30 ms para evitar el rebote del teclado */


delay_ms( DEBOUNCER_DELAY );

/* Se comienza activando una columna */


switch( column )
{
case 0:
/* Se pone a 1 la columna 0 */
port_B = COLUMN_0;
break;
case 1:
/* Se pone a 1 la columna 1 */
port_B = COLUMN_1;
break;
case 2:
/* Se pone a 1 la columna 2 */
port_B = COLUMN_2;
break;
case 3:
/* Se pone a 1 la columna 3 */
port_B = COLUMN_3;
break;
}

/* Se rastrea si hay pulsación en la columna activa */


if( ROW_0 == 1 )
{
keyPress = matrixKeyBoard[ 0 ][ column ];
}
else if( ROW_1 == 1 )
{
keyPress = matrixKeyBoard[ 1 ][ column ];
}
else if( ROW_2 == 1 )
{
keyPress = matrixKeyBoard[ 2 ][ column ];
}

71
else if( ROW_3 == 1 )

{
keyPress = matrixKeyBoard[ 3 ][ column ];
}

if( 3 == column )
{
/* Se completo un ciclo, reiniciamos nuevamente */
column = 0;
}
Else
{
column++;
}

return( keyPress );
}

Las características de esta primera versión son:

 Sólo es válido para el puerto B, por lo tanto hay que tener en cuenta una posible inclusión
de resistencias PULLUP.
 Se mete un retardo de 30 ms para evitar el problema del rebote en las pulsaciones del
teclado. Este parámetro podríamos configurarlo en la llamada a la función de inicio.
Esta librería funciona perfectamente en Proteus (aunque habría que probarla en un circuito real),
además está perfectamente explicada. A continuación se presenta una pequeña modificación en
el código para que se pueda utilizar indistintamente en el Puerto B ó D:
/*******************************************************************************
* Modificación Nº1 de la Librería_Teclado 4X4 *
* *
* DESCRIPCIÓN: Esta modificación permite conectar el tecaldo al puerto B ó al D*
* *
*******************************************************************************/

//Para usar el puerto B descomenta la siguiente línea

//#define uso_puertoB_teclado4x4 TRUE

#if defined uso_puertoB_teclado4x4


#byte puerto = 6
#bit ROW_3 = 0x06.7 // Identificadores de bits puerto B
#bit ROW_2 = 0x06.6
#bit ROW_1 = 0x06.5
#bit ROW_0 = 0x06.4

#else
#byte puerto = 8
#bit ROW_3 = 0x08.7 // Identificadores de bits puerto D
#bit ROW_2 = 0x08.6
#bit ROW_1 = 0x08.5
#bit ROW_0 = 0x08.4

#endif

/* DEFINICIÓN DE CONSTANTES SIMBÓLICAS ****************************************/


#define COLUMN_0 1
#define COLUMN_1 2
#define COLUMN_2 4
#define COLUMN_3 8
#define NUMBER_COLUMNS 4
#define NUMBER_ROWS 4
#define DEBOUNCER_DELAY 30
72
/* DEFINICIÓN DE CONSTANTES ************************************/
char const matrixKeyBoard[ NUMBER_ROWS ][ NUMBER_COLUMNS ] =
{
/* Disposición del teclado */
'1', '2', '3', 'F',
'4', '5', '6', 'E',
'7', '8', '9', 'D',
'A', '0', 'B', 'C',
};

/* DEFINICIÓN DE VARIABLES LOCALES *******************************/


static unsigned column;

/* FUNCIONES PÚBLICAS *******************************************/


void mkd_int( char x )
{
switch( x )
{
case 'B':
set_tris_b( 0xF0 ); /* <RB7:RB4> como entrada, <RB3:RB0> como salida */
break;
case 'D':
set_tris_d( 0xF0);/* <RD7:RD4> como entrada, <RD3:RD0> como salida */
break;
}
puerto = 0;
column = 0;

char mkd_keyPress( void )


{
static char keyPress = '\0';

/* Se ha de esperar aprox. 30 ms para evitar el rebote del teclado */


delay_ms( DEBOUNCER_DELAY );

/* Se comienza activando una columna */


switch( column )
{
case 0:
/* Se pone a 1 la columna 0 */
puerto = COLUMN_0;
break;
case 1:
/* Se pone a 1 la columna 1 */
puerto = COLUMN_1;
break;
case 2:
/* Se pone a 1 la columna 2 */
puerto = COLUMN_2;
break;
case 3:
/* Se pone a 1 la columna 3 */
puerto = COLUMN_3;
break;
}

/* Se rastrea si hay pulsación en la columna activa */


if( ROW_0 == 1 )
{
keyPress = matrixKeyBoard[ 0 ][ column ];
}
else if( ROW_1 == 1 )
{
keyPress = matrixKeyBoard[ 1 ][ column ];
}

73
else if( ROW_2 == 1 )
{
keyPress = matrixKeyBoard[ 2 ][ column ];
}
else if( ROW_3 == 1 )
{
keyPress = matrixKeyBoard[ 3 ][ column ];
}

if( 3 == column )
{
/* Se completo un ciclo, reiniciamos nuevamente */
column = 0;
}
Else
{
column++;
}

eturn( keyPress );
}

Programa principal:

*--------------------------------------------------------*\
| Comunicación entre dos PIC's por USART. Transmisor |
| |
| |
\*--------------------------------------------------------*/

#include <16F877.h>
#use delay (clock = 4000000)

#FUSES NOWDT, XT, NOPUT, NOPROTECT, NOBROWNOUT, NOLVP, NOCPD, NOWRT, NODEBUG

#use rs232(uart1, baud=9600)//usart1 -->ajuste de XMIT y RCV para la USART 1

#include "Libreria_Teclado4X4.c"

void main() {
char c;

//mkd_int('B'); //inizializo drivers teclado para puerto B


mkd_int ('D'); //inicializo drivers teclado para puerto D
while(true)
{
c = mkd_keyPress();
if(c != 0 )//si se ha pulsado una tecla
{
putc(c);//Envía el caracter vía serie por la USART del PIC
}
}
}

Ejemplo

El siguiente ejemplo, almacena dos caracteres en la EEPROM del receptor:

TRANSMISOR

#include <16F628A.h>

#FUSES NOWDT, XT, NOPUT, NOPROTECT, NOBROWNOUT, NOMCLR, NOLVP, NOCPD


#use delay(clock=4000000)
#use rs232(baud=9600,parity=N,xmit=PIN_B2,rcv=PIN_B1,bits=8)
74
void main()
{
char dato1='1';
char dato2='2';
putc(dato1);
putc(dato2);
}

RECEPTOR

#include <16F628A.h>

#FUSES NOWDT, XT, NOPUT, NOPROTECT, NOBROWNOUT, NOMCLR, NOLVP, NOCPD


#use delay(clock=4000000)
#use rs232(baud=9600,parity=N,xmit=PIN_B2,rcv=PIN_B1,bits=8)

//declaracion de variables globales


int contador=0;

#int_RDA
void RDA_isr(void)
{
char cdecenas,cunidades;

if (contador ==0)
{
cdecenas=getch();
write_eeprom(0,cdecenas);
contador++;
}
else
{
cunidades=getch();
write_eeprom(1,cunidades);
}
}

void main()
{
enable_interrupts(INT_RDA);
enable_interrupts(GLOBAL);
}

Y aquí tienes una imagen del circuito simulado:

75
76

Potrebbero piacerti anche