Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
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 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);
}
Se va analizar línea por línea el contenido del source LED.C, desde de la primera línea de
código.
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.
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.
6
/******************************************************************************
* *
* DESCRIPCIÓN: Decodificador de binario a decimal *
* Bin_Dec.c *
* *
******************************************************************************/
#include <16F877.h>
void main()
{
set_tris_b( 0x00 ); // Se configura port B como salida.
set_tris_a( 0x3F ); // Se configura port A como entrada
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.
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
/******************************************************************************
* *
* 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.
• 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.
10
Tabla de configuraciones de la entrada del A/D
/******************************************************************************
* *
* DESCRIPCIÓN: Conversión Análoga Digital *
* ConversorAD.c *
* *
******************************************************************************/
#include <16f877.h>
#include <reg.h>
#use delay(clock=4000000)
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;
port_B=0x00;
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 arranca_AD(void)
{
bit_set(ADCON0,2); //ponemos en marcha el conversor
}
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
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
INTCON
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.
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 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
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.
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);
ext_int_edge(flanco)
Ejemplo:
Si no queremos utilizar las funciones de CCS. Podemos modificar directamente el bit INTEDG
Ejemplo:
#bit INTEDG=0x81.6
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:
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.
/*--------------------------------------------------------*\
| Ejemplo uso de interrupción externa por RB0 |
| |
| InterrupcionRB0.c |
\*--------------------------------------------------------*/
#include <16F877.h>
#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.
INTCON
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.
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>
#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.
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.
enable_interrupts(INT_RB);
enable_interrupts(GLOBAL);
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á:
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.
El valor del registro INTCON según la secuencia del programa será la siguiente:
Al producirse la interrupción:
Al salir de la interrupción:
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:
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.
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.
Bien, primeramente vamos a ver como se tiene que configurar el registro OPTION para que el
TMR0 trabaje de esta forma:
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.
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.
#include <16F877A.h>
#fuses XT,NOWDT
#use delay(clock=4000000)
#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);
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.
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.
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
INTCON
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.
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.
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:
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.
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.
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).
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.
}
enable_interrupts(INT_TIMER0);
enable_interrupts(GLOBAL);
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.
#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:
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);
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.
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.
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:
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.
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:
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:
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:
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
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:
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.
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:
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.
#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:
CCS también incorpora funciones para leer y escribir en memorias EEPROM externas:
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.
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.
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.
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. }
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.
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.
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.
/******************************************************************************
* *
* DESCRIPCIÓN: Uso de la memoria EEPROM del PIC *
* EEPROM.c *
* *
******************************************************************************/
#include <16F877.h>
#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
//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 D2 se prende por tres ciclos del LED D1 cuando se presiona una tecla cualquiera de
las líneas RB4 a RB7.
/******************************************************************************
* *
* 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
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);
}
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);
}
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.
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.
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:
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.
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.
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. }
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:
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:
poner:
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>
#int_EXT
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
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.
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:
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
#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:
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.
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.
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.
/******************************************************************************
* *
* *
* 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
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 );
}
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*
* *
*******************************************************************************/
#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
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
#include "Libreria_Teclado4X4.c"
void main() {
char c;
Ejemplo
TRANSMISOR
#include <16F628A.h>
RECEPTOR
#include <16F628A.h>
#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);
}
75
76