Documente Academic
Documente Profesional
Documente Cultură
Introducción
Bienvenidos al inicio del Tutorial sobre PICs. Estas páginas te llevaran desde la estructura
básica del dispositivo, hasta los métodos y técnicas de programación. También habrá
sugerencias de como modificar el código para que lo puedas adaptar el PIC a tus propias
aplicaciones. No incluiré diagramas de arquitectura interna, ya que esto puede llevar a
confusiones. Si quieres echar un vistazo a la 'datasheet', la puedes bajar del sitio de Microchip.
Hay diversas formas de programar el PIC, - usando BASIC, C, o Lenguaje Ensamblador. Voy a
mostrarte el Lenguaje Ensamblador. No te asustes. Solo hay 35 instrucciones que aprender, y
es la manera más económica de programar los PICs, ya que no necesitas ningún otro software
extra que no sea de los gratuitos.
RA0 a RA4
RA es un puerto bidireccional. Eso quiere decir que puede ser configurado como entrada o
como salida. El número que hay después de RA indica el numero de bit (0 a 4). Por tanto,
tenemos un puerto bidireccional de 5 bits donde cada bit puede ser configurado como entrada o
como salida.
RB0 a RB7
VSS y VDD
Estos son los pins de alimentación. VDD es la alimentación positiva, y VSS es el negativo de la
alimentación, o 0 Voltios. La tensión máxima de alimentación que puedes utilizar son 6 Voltios,
y el mínimo son 2 Voltios.
OSC1/CLK IN y OSC2/CLKOUT
Estos pines son donde conectaremos el reloj externo, para que el microcontrolador disponga de
algún tipo de temporización.
MCLR
Este pin se utiliza para borrar las posiciones de memoria dentro del PIC (p.ej. cuando quiero
reprogramarlo). Durante el funcionamiento normal está conectado a la alimentación positiva.
INT
Este es un pin de entrada que puede ser monitorizado. Si el pin se pone a nivel alto, podemos
hacer que el programa se reinicie, se pare o cualquier otra función de deseemos. No lo
utilizaremos mucho.
TOCK1
Esta es otra entrada de reloj, que opera con un temporizador interno. Opera aisladamente del
reloj principal. De nuevo, este tampoco lo utilizaremos mucho.
Si prefieres el método "hazlo tu mismo", te recomendaría este sitio. Pulsa sobre "Supported
Programmers" para ver los circuitos. El más económico es el "TAIT Classic Programmer". El
software para programar el PIC también lo puedes bajar de esa página, ves a "Download".
Si quieres ir por la vía fácil, echa un vistazo a este sitio: (falta el sitio, el del texto original no
funciona).
Otro buen sitio de software gratuito es este. Este permite utilizar cualquier programador, puesto
que el software es completamente configurable.
Cualquier método funcionará, ya que ambos darán el mismo resultado, programar el PIC.
También recomiendo utilizar una placa de inserción para hacer tus circuitos, mientras juegas
con el PIC. Hay varios tamaños disponibles.
A continuación veremos como conectar un circuito simple para el desarrollo con el PIC.
La linea de alimentación está puesta a 6 Voltios, que es el máximo voltaje para el PIC. Puedes
utilizar cualquier voltaje inferior, hasta un mínimo de 2 Voltios. C3 es conocido como un
condensador de 'bypass'. Todo lo que se hace C3 es reducir el ruido de la linea de
alimentación. X1 es un cristal de 4 MHz. Puedes utilizar un circuito RC (resistencia y
condensador) (Nota de edición: crear enlace aquí), pero el precio del cristal es insignificante, y
es mas estable. C1 y C2 ayudan a reducir cualquier desviación en la oscilación cristal, y a
eliminar cualquier ruido no deseado antes de que la señal llegue al PIC.
Buenas técnicas para programar
Antes de meternos en harina con la programación del PIC, creo que ahora es un buen
momento para explicar algunas técnicas para programar bien.
Segundo, puedes asignar nombres a las constantes vía los registros (hablaremos de estos más
adelante). Hace lo que estás escribiendo mucho más sencillo de leer, para saber de que valor
se trata, mas que intentar entender que significan todos esos números. Así que utiliza nombres
reales como CONTADOR. Date cuenta de que hemos puesto el nombre en letras mayúsculas.
Esto lo hace destacar, y también significa (por convención) que se trata de una constante.
Tercero, añade algún tipo de cabecera en tus programas utilizando los punto y coma. Un
ejemplo sería algo así:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Autor:
;
; Fecha:
;
; Versión:
;
; Titulo:
;
;
;
; Descripción:
;
;
;
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Date cuenta de que hemos hecho una especie de caja utilizando puntos y comas. Esto es
simplemente para hacerlo más pulcro.
Finalmente, prueba y documenta el programa sobre papel también. Puedes usar o bien
diagramas de flujo o bien algoritmos o lo que tu quieras. Esto te ayudará a escribir tu programa
paso a paso.
Bien, eso es todo al respecto, vamos a entrar en materia.
Los Registros
Un registro es un lugar dentro del PIC que puede ser escrito, leído o ambas cosas. Piensa en
un registro como si fuese un trozo de papel donde tu puedes ver la información o escribirla.
La figura de más abajo muestra el mapa de registros del interior del PIC16F84. No te
preocupes si no has visto nada parecido antes, es solo para mostrar donde están los diferentes
bits y piezas dentro del PIC, y nos ayudará a explicar unos cuantos comandos.
La primera cosa que notarás es que está dividido en dos - Banco 0 y Banco 1. El Banco 1 es
utilizado para controlar las propias operaciones del PIC, por ejemplo para decirle al PIC cuales
bits del Puerto A son entradas y cuales son salidas. El Banco 0 se utiliza para manipular los
datos. Un ejemplo es el siguiente: Digamos que queremos poner un bit del puerto A a nivel alto.
Lo primero que necesitamos hacer es ir al Banco 1 para poner ese bit o pin en particular en el
puerto A como salida. Después volvemos al Banco 0 y enviamos un 1 lógico a ese pin.
Los registros que vamos a usar mas comunes en el Banco 1 son STATUS, TRISA y TRISB. El
primero permite volver al Banco 0, TRISA nos permite establecer los pines que serán entradas
y los que serán salidas del Puerto A, TRISB nos permite establecer los pines que serán
entradas y los que serán salidas del puerto B.
STATUS
Para cambiar del Banco 0 al Banco 1 utilizamos el registro STATUS. Hacemos esto poniendo el
bit 5 del registro STATUS a 1. Para cambiar de nuevo al Banco 0, ponemos el bit 5 del registro
STATUS a 0. El registro STATUS se localiza en la dirección 03h (la 'h' significa que el número
es hexadecimal).
TRISA y TRISB
Están localizados en las direcciones 85h y 86h respectivamente. Para programar que un pin
sea una salida o una entrada, simplemente enviamos un 0 o un 1 al bit en cuestión en el
registro. Ahora, podemos hacer esto ya sea en binario o en hexadecimal. Personalmente uso
ambos, ya que el binario ayuda mucho a visualizar el puerto. Si no estás familiarizado con el
paso de binario a hexadecimal y viceversa, utiliza una calculadora científica.
Entonces en el puerto A tenemos 5 pines, por tanto 5 bits. Si deseamos poner uno de los pines
como entrada, enviamos un 1 al bit en cuestión. Si deseamos poner uno de los pines como
salida, ponemos un 0 en ese bit. Los bits están definidos de manera correspondiente con los
pines, en otras palabras el bit 0 es el RA0, el bit 1 es el RA1, el bit 2 es el RA2, y así
sucesivamente. Vamos a tomar un ejemplo. Si queremos poner RA0, RA3 y RA4 como salidas,
y RA1 y RA2 como entradas, enviamos esto: 00110 (06h). Date cuenta de que el bit cero está a
la derecha, como se muestra aquí:
Numero de bit 4 3 2 1 0
Valor Binario 0 0 1 1 0
PORTA y PORTB
Para poner uno de nuestros pines de salida a nivel alto, simplemente ponemos un 1 el bit
correspondiente en nuestro registro PORTA o PORTB. El formato es el mismo que para los
registros TRISA y TRISB. Para leer si un pin está a nivel alto o nivel bajo en los pines de
nuestro puerto, podemos ejecutar un chequeo para ver si el bit en particular correspondiente
esta puesto a nivel alto (1) o está puesto a nivel bajo (0).
Antes de dar un ejemplo de código, tenemos que explicar dos registros mas - W y F.
W
El registro W es un registro de propósito general al cual le puedes asignar cualquier valor que
desees. Una vez que has asignado un valor a ese registro, puedes sumarle cualquier otro valor,
o moverlo. Si le asignas otro valor a W, su contenido es sobrescrito.
Un ejemplo de código
Vamos a darte un ejemplo de código sobre lo que acabamos de aprender. No intentes compilar
esto todavía, lo haremos cuando hagamos nuestro primer programa. Simplemente estamos
intentado mostrar como se hace la programación de lo anterior y de paso presentando un par
de instrucciones. Vamos a poner el Puerto A como en el ejemplo anterior.
Lo primero, necesitamos cambiar del banco 0 al banco 1. Hacemos esto modificando el registro
STATUS, que está en la dirección 03h, poniendo el bit 5 a 1.
BSF 03h,5
La instrucción BSF significa en ingles "Bit Set F" (Poner a 1 un bit de la Memoria). La
letra F significa que vamos a utilizar una posición de memoria, o un registro en memoria.
Vamos a utilizar dos números después de esta instrucción - 03h, el cual se refiere a la dirección
del registro STATUS, y el número 5 que corresponde al número de bit. Por tanto, lo que
estamos diciendo es "pon a 1 el bit 5 de la dirección de memoria 03h".
MOVLW b'00110'
Estamos poniendo el valor binario 00110 (la letra 'b' significa que el número está en binario) en
nuestro registro de propósito general W. Podríamos haber hecho esto en hexadecimal, en cuyo
caso nuestra instrucción hubiese sido:
MOVLW 06h
Cualquiera de las dos funcionará. La instrucción MOVLW significa en ingles "Move Literal Value
into W", en castellano, mover un valor literal directamente al registro W.
Ahora necesitamos poner este valor en el registro TRISA para configurar el puerto:
MOVWF 85h
Esta instrucción significa "poner los contenidos de W en el registro cuya dirección viene a
continuación", en este caso la dirección 85h, que apunta a TRISA.
Valor Binario 0 0 1 1 0
Entrada/Salida S S E E S
Ahora tenemos que configurar los pines del Puerto A, y para ello necesitamos volver al banco 0
para manipular cualquier dato.
BCF 03h,5
Esta instrucción hace lo contrario a BSF. Significa en ingles "Bit Clear F" (en castellano, poner
a 0 un bit de la memoria). Los dos números que le siguen son la dirección del registro, en este
caso del registro STATUS, y el número de bit, es este caso el 5. Así que lo que hemos hecho
ahora es poner a 0 el bit 5 del registro STATUS.
Léelo hasta que las entiendas. De momento ya hemos visto 4 instrucciones. ¡Solo nos quedan
31 para terminar!
Esto te sonará del apartado anterior. La única diferencia es que hemos puesto todos los pines
del Puerto A como salidas, poniendo 0h en el registro tri-estado (TRISA).
Ahora lo que tenemos que hacer es encender el LED. Hacemos esto poniendo uno de los pines
(aquel que tenga el LED conectado) a nivel alto. En otras palabras, enviamos un 1 al pin. Así es
como se hace (Mira los comentarios de cada linea):
Así que lo que hemos hecho ha sido encender y apagar el LED una vez.
Lo que queremos es que el LED se encienda y se apague continuamente. Para hacer esto
tenemos que volver al principio del programa. Para conseguir esto lo primero que hacemos es
poner una etiqueta al comienzo de nuestro programa, y diciéndole al programa que vaya a ese
punto constantemente.
Definimos una etiqueta muy simple. Escribimos un nombre, digamos INICIO, entonces el
código queda:
Como puedes ver, primer decimos la palabra 'Inicio' justo al comienzo del programa. Después,
justo al final del programa decimos simplemente 'goto Inicio', ves a Inicio. La instrucción 'goto'
significa en ingles 'ir a', y eso es lo que hace.
Este programa encenderá y apagará el LED constantemente, desde el momento que le demos
alimentación al circuito, y se detendrá cuando le quitemos la alimentación.
;
bsf 03h,5
movlw 00h
movwf 85h
bcf 03h,5
Inicio movlw 02h
movwf 05h
movlw 00h
movwf 05h
goto Inicio
Bien, he quitado los comentarios. Pero, ¿ te das cuenta de que todo lo que vemos son
instrucciones y números ? Esto puede ser algo confuso si mas tarde intentas depurar el
programa, y también cuando escribes código que te tengas que acordar de todas las
direcciones de memoria. Incluso con los comentarios puestos, puede ser un poco lioso. Lo que
necesitamos es dar nombres a estos números. Esto se consigue con otra instrucción: "equ"
La instrucción "equ" simplemente significa que algo equivale a algo [Nota de la traducción:
"equ" viene del termino ingles "equivalence", en castellano "equivalencia"]. No es una
instrucción del PIC, sino para el ensamblador. Con esta instrucción podemos asignar un
nombre a la dirección de localización de un registro, o en términos de programación asignar
una constante. Vamos a establecer algunas constantes para nuestro programa, y verás que
sencillo es de leer.
Así que ahora que hemos establecido nuestro valores constantes, vamos a ponerlos en nuestro
programa. Los valores constantes deben ser definidos antes de que los usemos. Para estar
seguros de ello los ponemos siempre al comienzo del programa. Reescribiremos de nuevo el
programa sin comentarios, para que puedas comparar el listado anterior con este:
Seguro que ahora puedes ver que las constantes hacen el programa un poco más sencillo,
aunque todavía no hemos puesto los comentarios. Sin embargo, no hemos terminado todavía.
Bucles de Retardo
Existe un ligero inconveniente en nuestro programa del LED parpadeante. Cada instrucción
necesita un ciclo de reloj para ser completada. Si utilizamos un cristal de 4 Mhz, cada
instrucción tardará 1/4 Mhz o 1 microsegundo en ser completada. Como solo estamos usando
5 instrucciones, el LED se encenderá y apagará en 5 microsegundos. Esto es demasiado
rápido para que lo podamos ver, y parecerá que el LED está permanentemente encendido. Lo
que necesitamos hacer es introducir un retardo entre el momento de encendido y apagado y
viceversa.
El principio para retardo es el de contar hacia atrás desde un número previamente establecido
y cuando llegue a cero, paramos de contar. El valor cero indica el fin del retardo y continuamos
nuestro camino a través del programa.
Así que lo primero que necesitamos hacer es definir una constante que usaremos como
contado. La llamaremos CONTADOR. Lo siguiente, necesitamos decidir el tamaño del número
desde el que contar. Bien, el número mayor que podemos tener es 255 o FFh en hexadecimal.
Ahora, como hemos mencionado en el apartado anterior, la instrucción equ asigna una palabra
a una localización de un registro. Esto significa que cualquiera que sea el número que
asignemos a CONTADOR, será igual al contenido de un registro.
Pero, sí, no llores, ¿ cómo ponemos un valor distinto en CONTADOR ? Bien, todo lo que
tenemos que hacer es primero 'mover' un valor a esta posición. Por ejemplo, si queremos que
CONTADOR tenga un valor de 85h, no podemos decir 'CONTADOR equ 85h' porque esta es la
localización del registro tri-estado del puerto A (TRISA). Lo que hacemos es esto:
Ahora, podemos decir 'CONTADOR equ 08h', CONTADOR será igual al valor 85h. Sutil,
¿ verdad ?
decfsz CONTADOR,1
Esta instrucción dice "resta 1 al registro (en esta caso CONTADOR). Si llegamos a cero, salta 2
lugares hacia delante"[Nota de la traducción: El valor que le sigue a la coma, indica donde
debe almacenarse el resultado de la operación. Si es 1, como en el ejemplo anterior, el
resultado se almacena en el mismo registro indicado en la instrucción, y si es 0 el resultado se
almacena en el registro w.] . Un montón de palabras para una sola instrucción. Veamosla en
acción antes, después la pondremos en nuestro programa.
CONTADOR equ 08h
ETIQUETA decfsz CONTADOR,1
goto ETIQUETA
;Continua por aquí.
:
:
:
Lo que hemos hecho es primero poner nuestra constante CONTADOR a 255. La siguiente linea
pone una etiqueta, llamada ETIQUETA seguida de nuestra instrucción decfsz. La instrucción
decfsz CONTADOR,1 disminuye el valor de CONTADOR en 1, y almacena el resultado de
vuelta en CONTADOR. También comprueba si CONTADOR tiene un valor de 0. Si no lo tiene,
hace que el programa salte a la siguiente linea. Aquí tenemos una instrucción de 'goto' que nos
envía de vuelta a nuestra instrucción decfsz. Si el valor de CONTADOR es igual a cero,
entonces la instrucción decfsz hace que el programa salte dos lugares hacia adelante, y se
sitúe donde hemos escrito "Continua por aquí". Así que, como puedes ver, hemos hecho que el
programa permanezca en un lugar durante un tiempo predeterminado antes de seguir adelante.
Esto se llama bucle de retardo. Si necesitamos un retardo mayor, podemos poner un bucle
detrás de otro. Cuantos mas bucles pongamos, mayor será el retardo. Nosotros vamos a
necesitar por lo menos dos, si queremos ver parpadear al LED.
; ...de nuevo.
;****Termina el Programa****
end ; Algunos compiladores
necesitan esta instrucción.
; y también por si acaso
olvidamos poner...
; ... la instrucción 'goto'.
Puedes compilar este programa y programar el PIC con él. Por su puesto, querrás probar el
circuito para ver si funciona realmente. Aquí está el diagrama del circuito para que lo
construyas una vez que hayas programado tu PIC:
Felicidades, acabas de escribir tu primer programa para PIC, y construido un circuito para
hacer parpadear un LED. Así que, si has seguido este tutorial hasta aquí, has aprendido 7
instrucciones de 35, y ya controlas los puertos de entrada/salida !
¿ Por qué no intentas modificar los bucles de retardo para hacer que el LED parpadee mas
rápido ? Cual es el valor mínimo de CONTADOR para poder ver el LED parpadear ? ¿ Por qué
no añades un tercer bucle o incluso más bucles de retardo después del primero para hacer más
lento el apagado mas lento ? Necesitarás una constante para cada bucle de retardo. Podrías
incluso ajustar tus bucles de retardo para hacer que el LED parpadease con un ritmo definido,
por ejemplo una vez por segundo.
En la siguiente sección veremos como podemos usar una cosa llamada sub-rutina para ayudar
a mantener el programa simple y pequeño.
Subrutinas
Una subrutina es una sección de código o programa, que puede ser llamada como y cuando la
necesites. Las subrutinas se usan si vas a ejecutar la misma función función más de una vez,
por ejemplo para crear un retardo. Las ventajas de utilizar una subrutina son que hará más
sencillo modificar el valor una vez dentro de la subrutina antes que, digamos, hacerlo diez
veces a través de tu programa. Y también te ayudará a reducir el total de memoria que ocupa
tu programa dentro del PIC.
Primero tenemos que dar un nombre a la subrutina, y en este caso hemos elegido RUTINA.
Después escribimos el código que queramos como hacemos normalmente. En este caso,
hemos elegido el código del retardo para el programa de parpadeo de nuestro LED.
Finalmente, terminamos la subrutina tecleando la instrucción RETURN.
Para arrancar la subrutina desde cualquier punto de nuestro programa, simplemente escribimos
la instrucción CALL seguida por el nombre de la subrutina.
Vamos a ver esto con algo más de detalle. Cuando alcanzamos la parte de nuestro programa
que dice CALL xxx, donde xxx es el nombre de nuestra subrutina, el programa salta a donde
quiera que resida la subrutina xxx. Las instrucciones dentro de la subrutina se ejecutan.
Cuando se alcanza la instrucción RETURN, el programa salta de vuelta a nuestro programa
principal, justo a la instrucción que va inmediatamente después de nuestra instrucción CALL
xxx.
Puedes llamar a la misma subrutina tantas veces como quieras, esa es la razón por la que
utilizar subrutinas reduce el tamaño total de nuestro programa. Sin embargo, hay dos cosas
que debes tener en cuenta. La primera, igual que en tu programa principal, cualquier constante
debe ser declarada antes de utilizarla. Pueden ser declaradas dentro de la subrutina misma, o
justo al comienzo del programa principal. Recomendaríamos que declarases todo al comienzo
del programa principal, para que así sepas que todo se encuentra en el mismo sitio. Lo
segundo, te debes asegurar de que el programa principal pasa por alto la subrutina. Lo que
queremos decir con esto es que si pones la subrutina justo al final del programa principal, a
menos que uses una instrucción 'goto' para saltar la subrutina, el programa seguirá y ejecutará
la subrutina tanto si quieres como si no. El PIC no diferencia entre una subrutina y el programa
principal.
Vamos a verlo en nuestro programa de parpadeo de LED, pero esta vez utilizaremos una
subrutina para el bucle de retardo. Con suerte verás que sencillo se queda el programa, y
también veras como funciona la subrutina en la realidad:
; ...de nuevo.
;
;**** Aquí está nuestra Subrutina
Retardo
Bucle1 decfsz CONTADOR1,1 ; Este segundo bucle mantiene el
LED...
goto Bucle1 ; ...apagado el tiempo
suficiente...
decfsz CONTADOR2,1 ; ...para que lo veamos
goto Bucle1 ;
return
;
;**** Fin del Programa****
end ; Algunos compiladores necesitan
esta instrucción,
; y también por si acaso
olvidamos poner...
; ... la instrucción 'goto'.
Puedes ver que utilizando una subrutina para nuestro bucle de retardo, hemos reducido el
tamaño del programa. Cada vez que queramos hacer un retardo, ya sea cuando el LED esté
apagado o cuando esté encendido, simplemente llamamos a la subrutina de retardo. Al final de
la subrutina, el programa retorna a la linea siguiente a la instrucción 'call'. En el ejemplo
anterior, encendemos el LED. Después llamamos a la subrutina. Entonces el programa retorna
para que podamos apagar el LED. Llamamos a la subrutina de nuevo, y cuando la surutina
termina, el programa retorna a la siguiente instrucción que ve, que es 'goto Inicio'.
Para aquellos de vosotros que estéis interesados, nuestro programa original tenia 120 bytes de
tamaño. Mediante el uso de la subrutina, hemos reducido el programa a 103 bytes.[Nota de la
traducción: Estos tamaños en bytes a los que se refiere el autor, son los tamaños de los
ficheros .HEX que resultan de compilar estos listados con el compilador de Microhip MPASM.]
Puede que no parezca gran cosa, pero teniendo en cuenta que solo tenemos 1024 bytes en
total dentro del PIC , cada pequeño bit ayuda.
[Nota de la traducción: Cuando el autor dice que el 16F84 tiene 1024 bytes, se esta refiriendo
al área de memoria para el almacenamiento del código o programas. Y, aunque utiliza la
palabra bytes aquí, en realidad no se trata de bytes(los cuales tienen 8 bits), sino que se trata
14-bit words(en castellano, palabras de 14-bits, un poco más de un byte). De modo, que lo que
sería correcto decir es, que el área de memoria de programa en el 16F84 tiene un tamaño
1024 x 14 bit words, o 1K x 14 bit words, o 1024 posiciones de 14 bits cada una. Como dato
adicional, cada instrucción del conjunto de instrucciones del 16F84 ocupa una palabra de 14
bits. Es decir, cada una ocupa una de esas 1024 posiciones de memoria disponible.Microchip
fabrica otros PICs con mayor espacio de memoria interna. Estos se pueden ver en las páginas
de Microchip. ]
Si recuerdas de los capítulos previos, para configurar los puertos de E/S, tenemos que
cambiarnos del Banco 0 al Banco 1. Hagamos eso primero:
Ahora hemos puesto el bit 0 del puerto A como entrada. Lo que necesitamos hacer ahora es
comprobar si el pin está a nivel alto o a nivel bajo. Para ello, podemos usar una de estas dos
instrucciones: BTFSC y BTFSS.
La instrucción BTFSC significa "Haz una comprobación de bit en el registro y bit que
especificamos. Si es un 0, entonces sáltate la siguiente instrucción".
La instrucción BTFSS significa "Haz una comprobación de bit en el registro y bit que
especificamos. Si es un 1, entonces sáltate la siguiente instrucción".
La que usemos dependerá de como queramos que nuestro programa reaccione cuando lea la
entrada. Por ejemplo, si simplemente estamos esperando que la entrada sea 1, entonces
podríamos utilizar la instrucción BTFSS de este modo:
;Aquí el código
:
BTFSS PortA,0
Goto Inicio
;Continua por aquí
:
:
El programa solo se moverá hacia 'Continua por aquí' si el bit 0 del puerto A se pone a 1.
Vamos ahora a escribir un programa con el que el LED parpadeará a una velocidad, pero si un
conmutador[Nota de la traducción: en ingles el término original es "switch"] se cierra,
parpadeará a la mitad de velocidad. Seguramente puedas hacer el programa por ti mismo, pero
hemos incluido el listado de todos modos. Podrías probar a escribir el programa completo, solo
para ver si has comprendido los conceptos. Estamos usando el mismo circuito que antes, con
un conmutador añadido al pin RA0 del PIC y a la linea de alimentación positiva.
; ...de nuevo.
;
;**** Aquí está nuestra Subrutina
Retardo
Bucle1 decfsz CONTADOR1,1 ; Este segundo bucle mantiene el
LED...
goto Bucle1 ; ...apagado el tiempo
suficiente...
decfsz CONTADOR2,1 ; ...para que lo veamos
goto Bucle1 ;
return
;
;**** Fin del Programa****
end ; Algunos compiladores necesitan
esta instrucción,
; y también por si acaso
olvidamos poner...
; ... la instrucción 'goto'.
Lo que hemos hecho aquí es encender el LED. Después comprobar si el conmutador está
cerrado. Si está cerrado, entonces hacemos una llamada a nuestra subrutina de retardo. Esto
nos da el mismo retardo que anteriormente, pero ahora la estamos llamando dos veces. Lo
mismo pasa cuando el LED está apagado. Si el conmutador no está cerrado, entonces
tenemos nuestros tiempos de encendido y apagado como antes.
Puedes compilar y ejecutar este programa. Sin embargo, una nota de advertencia: El circuito
final y el código puede resultar irrelevante para alguien que no le interese programar
microcontroladores. Por tanto, no te enfades si cuando enseñes el circuito a tu familia y amigos
sobre como puedes cambiar la velocidad de parpadeo de un LED, ves que no muestran ni el
mas mínimo interés ¡ Hablamos desde nuestra experiencia personal !
Si has ido siguiendo estos capítulos desde el comienzo, puede que te interese saber que
¡ahora llevas aprendidas 10 de 35 instrucciones para el PIC 16F84 ! Y todas ellas las has
aprendido simplemente con el encendiendo y apagando de un LED.
Hasta ahora hemos hecho que el PIC haga parpadear un LED. Después fuimos capaces de
interactuar con nuestro PIC añadiendo un conmutador, para modificar el ritmo de parpadeo. El
único problema es que el programa es muy largo y desperdicia mucha memoria. Era aceptable
ya que introducíamos comandos por primera vez, pero debe existir una manera mejor de
hacerlo. Bueno, la hay (sabias que la había, ¿verdad? ).
movlw 02h
movwf PORTA
movlw 00h
movlw PORTA
Primero cargamos nuestro registro w con 02h, después lo pusimos en nuestro registro del
puerto A para encender el LED. Para apagarlo, cargamos w con 00h y después lo pusimos en
nuestro registro del puerto A. Entremedias de estas rutinas teníamos que llamar a una
subrutina para que pudiéramos ver el LED parpadear. Así que hemos tenido que mover dos
conjuntos de datos dos veces (una vez al registro w y después al PORTA) y llamar a la
subrutina dos veces (una vez para el encendido y otra para el apagado).
Así que, ¿ cómo podemos hacer esto de una manera mas eficiente ? Sencillo. Utilizamos otra
instrucción llamada XORWF.
La instrucción XORWF ejecuta una función OR Exclusiva entre el registro w y el registro que le
pasamos como dato. Será mejor que expliquemos que narices es una OR Exclusiva antes de
continuar. [Nota de la traducción: También llamada función XOR simplemente]
Si tenemos dos entradas, y una salida, la salida solo será 1 si, y solo si, las dos entradas son
diferentes. Si son iguales, la salida será 0. Aquí está la tabla de verdad, para aquellos que
prefieran verlo en una tabla:
A B Salida
0 0 0
0 1 1
1 0 1
1 1 0
Vamos a ver que ocurre si hacemos que B tome el valor de salida anterior, y simplemente
cambiamos el valor de A:
A B Salida
0 0 0
1 0 1
1 1 0
1 0 1
Así que ahora, para encender y apagar nuestro LED, necesitamos solo dos lineas:
MOVLW 02h
XORWF PORTA,1
Lo que estamos haciendo aquí es cargar nuestro registro w con 02h. Después le hacemos una
OR exclusiva a este número que hay en w con lo que quiera que esté en nuestro registro del
puerto A. Si el bit 1 es 1, cambiará a 0. Si el bit 1 es 0, cambiará a 1. [Nota de la traducción: El
número que va después del registro especificado en la instrucción XORWF, indica donde debe
de ser almacenado el resultado de dicha operación OR exclusiva. Si, como ocurre en este
ejemplo anterior, ponemos un 1, el resultado se almacenará de vuelta en el registro de
memoria especificado. Si pusiésemos 0, el resultado de la operación OR exclusiva se
almacenaría en el registro w]
Vamos a ejecutar este código un par de veces, para mostrar como funciona en binario:
PORTA
00010
xorwf 00000
xorwf 00010
xorwf 00000
xorwf 00010
Incluso no necesitamos cargar el mismo valor en nuestro registro w cada vez, lo podemos
hacer una sola vez al principio, y simplemente saltar hacia atrás a nuestro comando de
conmutación. Además, no tenemos que establecer una valor al registro de nuestro puerto A.
¿ Por qué ? Bien, si durante el encendido es un 1, lo haremos conmutar. Si por el contrario es
un 0 durante el arranque, también lo conmutaremos.
Así que, vamos a ver nuestro nuevo código. El primero es nuestro código original de parpadeo
del LED, y el segundo es donde hemos añadido el conmutador externo:
LED Parpadeante:
Esperamos que hayas podido comprender cómo con el uso de una simple instrucción, hemos
reducido el tamaño de nuestro programa. De hecho, simplemente para mostrar en cuanto se
han reducido nuestros programas, hemos mostrado los dos programas, los cambios que
hicimos, y sus tamaños en la siguiente tabla:
[Nota de la traducción: Estos tamaños en bytes a los que se refiere el autor, son los tamaños
de los ficheros .HEX que resultan de compilar estos listados con el compilador de Microhip
MPASM.]
Así que no solo hemos aprendido nuevas instrucciones, ¡también hemos reducido el tamaño de
nuestro código!
AND
La función AND simplemente compara dos bits y produce un 1 si son iguales, y 0 si son
diferentes. Por ejemplo, si decimos 1 AND 1, el resultado es 1, mientras que si decimos 1 AND
0 el resultado será 0. Por supuesto, podemos comparar palabras (o bytes) también, y la función
AND hace esta comparación de ambas bit a bit. El ejemplo de más abajo muestra dos palabras
de 8 bits a las que se aplica AND con el siguiente resultado:
11001011
AND 10110011
Igual a 10000011
Como puedes ver, el resultado solo tiene un 1 cuando dos 1s coinciden en ambas palabras.
Podemos utilizar la función AND para hacer comprobaciones de puertos, por ejemplo. Si
estamos monitorizando pines E/S que están conectados a un circuito, y tenemos que
monitorizar una condición en concreto donde solo algunos pines están a nivel alto, entonces
simplemente podemos leer el puerto, y aplicar a ese valor la función AND con la condición que
estamos buscando, simplemente como en el ejemplo anterior.
[Nota de la Traducción: Incluimos aquí la tabla de verdad de esta función que no estaba en el
texto original por homogeneizar:]
A B Resultado de AND
0 0 1
0 1 0
1 0 0
1 1 1
El PIC nos da dos modalidades para AND. Estas son ANDLW y ANDWF.
ANDLW nos permite hace una función AND con los contenidos del registro W, y un número que
nosotros especifiquemos. Las sintaxis es:
ANDLW <número> ; donde <número> es con el que haremos AND a los contenidos de W. El
resultado de la función AND serán almacenamos de vuelta en el registro W. [Nota de la
traducción: El valor de <número> tiene que estar comprendido entre 0 y 255]]
ANDWF nos permite hacer una función AND con los contenidos del registro W y otro registro,
como por ejemplo un puerto. Las sintaxis es:
Las dos secciones de código de más abajo muestran un ejemplo de cada instrucción AND. La
primera comprueba el estado del PORTA, donde necesitamos ver si las entradas son 1100.
Pondremos el resultado de vuelta en el registro W:
movlw 1100
ANDWF 05h,0
ANDLW 1100
OR
Ya hemos visto una función OR, llamada XOR. Esta produce un 1 si dos bits son diferentes,
pero no si son iguales. Hay una segunda función OR llamada IOR, la cual es OR inclusiva. La
función produce un 1 si cualquiera de los dos bits es 1, pero también si ambos bits son 1. Aquí
está la tabla de verdad para demostrar esto:
A B Resultado de OR
0 0 0
0 1 1
1 0 1
1 1 1
Operadores Aritméticos
ADD
Esta función exactamente lo que dice [Nota de la traducción: "Add" en ingles significa sumar]
¡Suma dos números! Si el resultado de sumar dos números excede de 8 bits [Nota de la
traducción: 8 bits pueden contener un valor máximo de 255 en decimal], entonces se activará
un flag llamado CARRY [Nota de la traducción: "flag" en castellano se podría traducir por
banderín, y normalmente se trata de 1 bit que se ubica en un registro en concreto de la
memoria del PIC. "CARRY" en castellano puede traducirse como "llevar", podría ser como
cuando hacemos una cuenta a mano y utilizamos la expresión "me llevo una".]. El flag de
CARRY está localizado en el bit 0 de la dirección de memoria 03h. Si este bit se activa, quiere
decir que la suma de esos 2 números excedió de 8 bits. Si es 0, entonces el resultado queda
dentro de los 8 bits.
De nuevo, el PIC nos da dos modalidades de ADD, que son ADDLW y ADDWF. Como puedes
suponer, es muy similar a la función anterior.
ADDLW añade los contenidos del registro W con un número que le especifiquemos. La sintaxis
es:
ADDLW <número>
ADDWF añadirá los contenidos del registro W y cualquier otro registro que le especifiquemos.
La sintaxis es:
Ahora, ¡apuesto a que sabes que hace esta función! Sí, lo que suponías, esta función sustrae(o
resta) un bit a otro ['Nota de la traducción: SUB, viene del ingles "substract" que significa
"sustraer" en castellano]]. Una vez más el PIC da dos modalidades: SUBLW y SUBWF. La
sintaxis es exactamente la misma que para la función ADD, excepto por supuesto ¡que tienes
que escribir SUB en lugar de ADD!
Incremento
movlw 01
addwf 0Ch,1
Hay una manera mejor de hacer esto. Podemos utilizar la instrucción INCF. La sintaxis es:
Mediante el uso de esta simple instrucción podemos literalmente hacer la mitad de código. Si
queremos que el resultado vaya de vuelta al registro W, usando el ejemplo anterior,
hubiésemos tenido que añadir otro comando para mover el contenido de la posición 0Ch de
vuelta al registro W, y después poner en el registro 0Ch lo que quiera que tuviese al principio.
Existe otro comando de incremento. Es el INCFSZ. Este comando incrementará el registro que
nosotros le especifiquemos, pero si el registro es igual a 0 después del incremento (esto
ocurrirá cuando añadamos 1 a 255) entonces el PIC se saltará la siguiente instrucción. La
sección de código de más abajo lo demuestra:
Decremento
Complemento
La última instrucción de este grupo invierte todos los bits de un registro que le especifiquemos.
La sintaxis es:
COMF <registro>,d ; donde <registro> es el registro que deseamos invertir, y d le dice al PIC
donde almacenar el resultado. Si d=0, el resultado se almacena en el registro W, y si d=1 el
resultado se almacena en ese registro especificado.
0Ch = 11001100
COMF 0Ch,1
0Ch = 00110011
Esto se puede usar, por ejemplo, para cambiar rápidamente todos los bits de un puerto de
salida a entra y viceversa.
Ya hemos visto un par de operaciones con un bits, cuando configurábamos los puertos del PIC,
y voy a repetir aquí su uso.
BCF
Esta instrucción pone a cero un bit que especifiquemos en el registro que especifiquemos. La
sintaxis es la siguiente:
BCF <registro>,<bit>
La hemos utilizado previamente para cambiar del Banco 1 al Banco 0 cuando poníamos a 0 el
bit 5 del registro STATUS. También podemos utilizarla para poner a 0 cualquier bit de cualquier
otro registro/posición de memoria. Por ejemplo, si queremos poner a 0 el tercer bit de
11001101 que está almacenado en la posición de memoria 0Ch, introduciríamos:
BCF 0Ch,2
Posición 8º bit 7º bit 6º bit 5º bit 4º bit 3er bit 2º bit 1er bit
Número de bit 7 6 5 4 3 2 1 0
Nuestro ejemplo 1 1 0 0 1 1 0 1
BSF
Esta instrucción pondrá a 1 el bit que especifiquemos en cualquier registro que especifiquemos.
Hemos utilizado esta operación antes para cambiar del Banco 0 al Banco 1. La sintaxis es:
BSF <registro>,<bit> , y se utiliza exactamente de la misma forma que la BCF de más arriba.
BTFSC
Hasta ahora hemos puesto a 1 o a 0 un bit en un registro. Pero ¿qué pasa si queremos
simplemente comprobar si un bit es 0 ó 1 en un registro? Bien, podemos utilizar BTFCS. Esta
dice "Comprueba un bit en el registro o posición de memoria (F), y salta si es 0". Esta
instrucción comprobará el bit que le especifiquemos en el registro. Si el bit es 0, la instrucción le
dice al PIC que se salte la instrucción que viene a continuación. Podríamos utilizar esta
instrucción si queremos comprobar un flag, como por ejemplo en flag de CARRY. Esto nos
ahorra tener que leer el registro STATUS y mirar los bits individualmente para ver que flags
están a 1. Por ejemplo, si queremos comprobar si el flag de CARRY ha sido puesto a 1 cuando
hayamos sumado dos números, haremos lo siguiente:
;
BTFSC 03h,0
<instrucción x> ; continua por aquí si CARRY está a 1.
<instrucción y> ; o por aquí si está a 0.
Si el valor del bit es 1, entonces BTFSC continuará por la instrucción inmediatamente siguiente.
Si está a 0, entonces se salta esa instrucción. La siguiente sección de código muestra donde
podría ser utilizada:
Bucle :
:
:
BTFSC 03,0
Goto Bucle
En el código anterior, el PIC solo saldrá de bucle si el bit 0 del registro STATUS (o el flag de
CARRY) está puesto a 0. De otro modo, el comando 'goto' será ejecutado.
BTFSS
Esta instrucción dice "Comprueba el bit del registro o posición de memoria(F), y salta si está a
1". Esta es similar a la instrucción BTFSC, excepto que el PIC se saltará la siguiente instrucción
si el bit que estamos comprobando está a 1, en lugar de a 0.
CLRF
Esta instrucción pondrá todos los bits del contenido de un registro a 0. La sintaxis es:
CLRF <registro>
La hemos usado anteriormente para poner todos los pines de un puerto como salidas, haciendo
"CLRF 85h". También lo usamos para poner los pines de un puerto que estaban como salidas
todos a 0, haciendo "CLRF 05h".
CLRW
CLRW
RLF y RRF
Estas instrucciones desplazan los bits del contenido de un registro un lugar hacia la izquierda
(RLF), o un lugar hacia la derecha (RRF). Por ejemplo, si tenemos 00000001 y utilizamos la
instrucción RLF, tendríamos 00000010. Ahora, ¿qué ocurre si tenemos 10000000 y empleamos
RLF? Bien, el bit 1 será desplazado al flag CARRY. y si empleamos RLF una vez más, el 1
reaparecerá justo al principio [Nota de la traducción: es decir, por el lado derecho del byte, o
dicho de otro modo en el bit0]. Lo mismo ocurre con la instrucción RRF pero de manera
inversa. El ejemplo de más abajo ilustra esto para la instrucción RLF, donde mostramos los 8
bits del contenido de un registro, y el flag de CARRY:
C 76543210
0 00000001
RLF 0 00000010
RLF 0 00000100
RLF 0 00001000
RLF 0 00010000
RLF 0 00100000
RLF 0 01000000
RLF 0 10000000
RLF 1 00000000
RLF 0 00000001
RLF <registro>,d
RRF <registro>,d
Programa de ejemplo
Ahora vamos a darte un ejemplo de código que puedes compilar y ejecutar. Este hará que una
luz que se desplace, comenzando en el bit 0 del puerto B hasta el bit 8 del mismo, siguiendo al
bit 0 del puerto A hasta el bit 5, y después haciendo todo el camino inverso hasta al principio.
Conecta LEDs a todos los pines de los puertos A y B. Aquí comprenderás algunas de las
operaciones de bit mencionadas en este capítulo:
Tablas de Datos
Hay una propiedad interesante en el conjunto de instrucciones del PIC, que nos va a permitir
realizar tablas de datos.
Una tabla de datos es una lista simple de valores de datos, donde cada uno de ellos puede
ser leído(solo leído) dependiendo de algún criterio.
[Nota de la traducción: El ejemplo dado en el texto original, se ha sustituido por este que
mostramos a continuación para hacer más comprensible el concepto de tabla de datos. Al final
del capítulo se incluye un programa para realizar el ejemplo:
Por ejemplo, podrías hacer un circuito con el PIC, que cuente de 0 a 9 y represente este
número en un display de 7 segmentos. El display de 7 segmentos lo vamos a conectar al
puerto B. De manera que cada pin encienda un LED del display (como tenemos 8 pines en el
puerto B, uno de ellos no sería utilizado). Para ello, podemos utilizar el siguiente esquema de
conexiones entre el PIC y el display:
Es decir, el pin RB7 no lo conectaríamos al display. De este modo tendremos la siguiente tabla
de correspondencias entre los números a representar y el valor binario que debe de tomar el
puerto B para que se enciendan los diodos LED correspondientes:
1 b'01000001' = 41h
2 b'00111011' = 3Bh
3 b'01101011' = 6Bh
4 b'01001101' = 4Dh
5 b'01101110' = 6Eh
6 b'01111110' = 7Eh
7 b'01000011' = 43h
8 b'01111111' = 7Fh
9 b'01101111' = 6Fh
0 b'01110111' = 77h
Ahora, antes de continuar con la explicación de como funciona la tabla de datos, tenemos que
explicar como hace el PIC seguimiento de por donde va el programa mientras se está
ejecutando. Si alguna vez has programado en BASIC esto te ayudará a comprenderlo. Si no,
no te preocupes, aun así serás capaz de comprender el concepto.
10 LET K=0
11 K=K+1
12 IF K>10 THEN GOTO 20 ELSE GOTO 11
20 PRINT K
21 END
El programa comienza en la linea 10. Una vez que K vale 0, continua con la linea 11. Después
de que le hemos añadido 1 a K nos desplazamos a la linea 12. Aquí estamos preguntando si K
es mayor que 10. Si lo es, entonces vamos a la linea 20, y si no, volvemos a la 11. La linea 20
muestra el valor de K en pantalla, y la linea 21 finaliza el programa. BASIC utiliza los números
de linea para ayudar al programador a hacer seguimiento de donde está cada cosa, ya que las
etiquetas no están permitidas.
El PIC utiliza etiquetas para saltar de unas posiciones a otras, ¿si?. Utilizamos las etiquetas
para que sepamos donde están las cosas, y también para que podamos decirle al PIC de una
manera sencilla donde tiene que ir. Lo que realmente ocurre es que el PIC utiliza un contador
de línea interno llamado Contador de Programa [Nota de la traducción: en ingles "Program
Counter", abreviado también como PC. ]. El Contador de Programa mantiene almacenada la
dirección de la posición de memoria donde se encuentra la instrucción actual. Cuando le
decimos al PIC que vaya a una etiqueta en particular, sabe la posición de memoria y por tanto
modifica el PC hasta que alcanza esa posición y la lee. Esto es exactamente el mismo modo en
el que leemos nosotros el programa BASIC anterior.
Aquí hay una sección de código, con las posiciones de memoria, o contenidos del PC, junto a
cada instrucción:
PC Instrucción
-----------------------------
0000 movlw 03h
0001 movwf 0Ch
0002 Bucle decfsc 0Ch
0003 goto Bucle
0004 end
En este ejemplo anterior hemos puesto el PC a 0000. En esta posición tenemos la instrucción
"movlw 03h". Cuando el PIC ha ejecutado esta instrucción, incrementará PC para que la
siguiente instrucción pueda ser leída. Aquí el PIC ve "movwf 0Ch". El PC se incrementa de
nuevo. Ahora el PIC lee "decfsc 0Ch". Si el contenido del registro 0Ch no es 0, entonces el PC
se incrementa en 1, y la siguiente instrucción, "goto Bucle", le dice al PC que vaya de vuelta a
la posición 0002, en la cual hemos escrito la palabra "Bucle". Si el contenido del 0Ch es 0,
entonces el PC será incrementado en 2, en otras palabras se saltará la siguiente instrucción.
Esto colocará el PC en la posición 0004, donde el programa termina.
Las posiciones son asignadas por el programa ensamblador (p.ej. MPASM), y normalmente no
nos tenemos que preocupar de lo que hace el Contador de Programa(PC). Hasta que
necesitemos controlarlo, como vamos a hacer cuando usemos tablas de datos.
La mejor manera de explicar como funciona una tabla de datos es comenzar con un pequeño
ejemplo.
PC equ 02h
;
movlw 03h
call tabla
:
:
:
tabla addwf PC
retlw 0Ah
retlw 0Bh
retlw 0Ch
retlw 0Dh
retlw 0Eh
retlw 0Fh
retlw 10h
;
return
La primera instrucción esta asignando a la etiqueta PC la dirección del Contador de Programa
(02h). Después colocamos el valor 03h en el registro w. A continuación hacemos una llamada a
"tabla". La primera linea en la subrutina "tabla" añade los contenidos del registro W (03h) al
Contador de Programa. Esto causa que el contador de programa se incremente en 3, o dicho
de otro modo, causa que el contador de programa se mueva 3 lineas hacia abajo. Cuando el
contador llega 3 lineas más abajo el PIC ve la instrucción retlw. Este comando pasa el valor
que viene con la instrucción, al registro W, y retorna desde la subrutina.
RETLW realmente significa "RETurn, y pon el Literal en W". Date cuenta de que hemos puesto
una coma después de la palabra "return" en la frase anterior. Como si estuviésemos en una
subrutina normal, necesitamos una instrucción de "return" para salir de ella. Eso lo hace
el RET en la instrucción. Después de la instrucción RETLW hay un número, y este es el que
colocaremos en el registro W. En este caso el número 0Ch.
[Nota de la traducción: En resumidas cuentas, el valor que ponemos en el registro W antes de
llamar a la subrutina "tabla", indica qué posición queremos mirar de la tabla. Es decir, actúa de
índice o indicador de la posición que queremos ver. Al pasarle un 3 a W antes de "call tabla", le
estamos diciendo que queremos recuperar el valor que existe en la posición 3 de la tabla. La
primera instrucción de la subrutina "tabla" dice "sumarle el contenido de W a PC", es decir, le
sumará 3 a PC. Lo cual va a trasladar el PC hasta la instrucción "retlw 0Ch". Entonces, este
valor, 0Ch, será copiado en el registro W, y se efectuará un "RETurn", que hace que el
programa continúe por la instrucción siguiente a "call tabla". Ahora, estamos de vuelta al flujo
principal del programa con W cargado con el dato que queríamos recuperar de nuestra tabla.
De este modo, hemos creado una tabla de datos, que la podremos consultar cuando queramos
para ver un valor de una posición concreta. En el ejemplo del display de 7 segmentos se ve de
modo práctico para qué se podría utilizar una tabla de datos.]
(Antes de llamar a la subrutina) podemos asignar cualquier número a W, siempre y cuando este
número al ser añadido a PC haga que PC se desplace aun elemento de la tabla de la subrutina,
allí donde hemos puesto una instrucción retlw. En el ejemplo anterior esto significa que
podemos poner un número de 1 a 7. Si nos pasamos de largo de la subrutina, podríamos
terminar ejecutando otra parte del programa. Por esta razón, siempre es buena idea poner la
tabla de datos al final del programa, así si nos pasamos de largo podemos llegar al final del
programa en cualquier caso.
[Nota de la traducción: Continuando con el ejemplo del display de 7 segmentos, ponemos aquí
la tabla de datos que tendremos que crear. La hemos sacado de la tabla del principio de este
capítulo:
Posción Valor Número para el display
1 b'01000001' = 41h 1
2 b'00111011' = 3Bh 2
3 b'01101011' = 6Bh 3
4 b'01001101' = 4Dh 4
5 b'01101110' = 6Eh 5
6 b'01111110' = 7Eh 6
7 b'01000011' = 43h 7
8 b'01111111' = 7Fh 8
9 b'01101111' = 6Fh 9
10 b'01110111' = 77h 0
Hemos puesto el 0 en la última posición, para que el resto de los números que hay que
representar en el display, coincidan con la posición que ocupan en la tabla.
De este modo, la tabla se podría crear utilizando el siguiente código:
PC equ 02h
:
:
tabla addwf PC
retlw 41h
retlw 3Bh
retlw 6Bh
retlw 4Dh
retlw 6Eh
retlw 7Eh
retlw 43h
retlw 7Fh
retlw 6Fh
retlw 77h
return
Hemos reutilizado parte del código que hemos empleado anteriormente para crear este nuevo.
Así que habrá partes que te serán familiares. Este es el código completo para hacer que el PIC
muestre en el display una cuenta ascendente de 0 a 9 y vuelta a empezar:
Obviamente, este programa es un simple ejemplo que puede ser optimizado con los conceptos
que has aprendido en los anteriores capítulos. ¿ podrías reducir el número de instrucciones ? ¿
Podrías hacer que la cuenta fuese ascendente o descendente dependiendo del valor de otro
pin del puerto A ? ¡¡Animo!! , esta es una buena ocasión de experimentar con lo que ya sabes.
Así que, ¿qué es una interrupción? Bien, como su nombre sugiere, una interrupción es un
proceso o una señal que hace detenerse al microprocesador/microcontrolador de lo está
haciendo, para que haga otra cosa. Permiteme darte un ejemplo de la vida cotidiana. Supón
que estás sentado en tu casa, charlando con alguien. De repente el teléfono suena. Tú
detienes la charla, levantas el teléfono y hablas con la persona que te llamó. Cuando terminas
tu conversación telefónica, vuelves para charlar con la persona que estabas antes de que
sonase el teléfono. Puedes hacerte la idea de que la rutina principal es que tú estás charlando
con alguien, que el teléfono suene causa una interrupción a tu charla, y que la rutina de
interrupción es el proceso de hablar por teléfono. Cuando la conversación telefónica ha
terminado, entonces vuelves a la rutina principal de la charla. Este ejemplo es exactamente
igual a como una interrupción hace que actúe un procesador. El programa principal está en
curso, ejecutando alguna función en un circuito, pero cuando ocurre una interrupción el
programa principal se detiene para que otra rutina se lleve a cabo. Cuando la rutina termina, el
procesador vuelve a la rutina principal de nuevo.
El PIC tiene 4 fuentes de interrupción. Se pueden dividir en dos grupos. Dos de ellas son
fuentes de interrupciones que pueden ser aplicadas externamente al PIC, mientras que las
otras dos son de procesos internos. Vamos a explicar las dos externas aquí. Las otras dos las
explicaremos en otros capítulos cuando miremos los temporizadores [Nota de la traducción:
timers] y el almacenamiento de datos.
Si observas los pines del PIC, verás que el pin 6 se muestra como RB0/INT. RB0 es
obviamente el bit 0 del Puerto B. El INT simboliza que también puede ser configurado como un
pin de interrupción. También, los bits del 4 al 7 del puerto B (pines 10 al 13) pueden ser
utilizados para interrupciones. Antes de que usemos INT u otros pines del puerto B,
necesitamos hacer dos cosas. La primera necesitamos decirle al PIC que vamos a usar las
interrupciones. Segunda, necesitamos especificar que pin del puerto B usaremos como
interrupción y no como un pin de Entrada/Salida.
Dentro del PIC hay un registro llamado INTCON, y su dirección es la 0Bh. Dentro de este
registro hay 8 bits que pueden ser habilitados o deshabilitados. El bit 7 de INTCON es llamado
GIE [Nota de la Traducción: en ingles de las siglas "Global Interrupt Enable", en castellano
"Habilitador de Interrupciones Global"]. Poniendo este bit a 1 le decimos al PIC que vamos a
usar una interrupción. El bit 4 de INTCON es llamado INTE [Nota de la traducción: en ingles,
"INT Enable". En castellano "Habilita INT", o dicho de otro modo que utilizaremos el pin 6 con
su función INT, no como RB0.]. Poniendo este bit a 1 decimos al PIC que RB0 será un pin de
interrupción. Poniendo a 1 el bit 3 de INTCON, llamado RBIE, decimos al PIC que usaremos los
bits del 4 al 7 del puerto B como interrupciones. Ahora el PIC sabe que cuando uno de estos
pines cambia a nivel alto o cambia nivel bajo, tiene que parar lo que esté haciendo e ir a una
rutina de interrupción. Ahora, tenemos que decirle al PIC si la interrupción se va a producir en
la transición ascendente de la señal(de 0 Voltios a +5 Voltios) o la descendente (de +5V a 0V).
En otras palabras, ¿queremos que el PIC haga la interrupción cuando la señal vaya de nivel
bajo a nivel alto, o de nivel alto a nivel bajo? . Por defecto, esto está configurado para que sea
en la transición ascendente (o flanco de subida de la señal). El flanco de "disparo" se configura
en otro registro llamado OPTION, que está en la dirección 81h.
El bit en el que estamos interesados es el bit 6, que se llama INTEDG [Nota de la traducción:
del ingles "INTerrupt EDGe", o en castellano "flanco para la interrupción" ]. Poniendo este a 1
causará que el PIC sea interrumpido en el flanco de subida o transición ascendente (el estado
por defecto) y poniéndolo a 0 causará que el PIC sea interrumpido en el flanco de bajada o
transición descendente. Si quieres que el PIC sea interrumpido en el flanco de subida, no
necesitas configurar el bit. Pero, desafortunadamente el registro OPTION está en el Banco 1, lo
que significa que tienes que cambiar del banco 0 al banco 1, cambiar el bit del registro
OPTION, y después volver al banco 0. El truco está en hacer todas las operaciones con los
registros del banco 1 de una sola vez, tales como configurar los pines de los puertos, etc... y
después volver al banco 0 cuando hayas terminado.
[Nota de la Traducción: A modo de esquema, esto es lo que hay que hacer por orden, para
utilizar las interrupciones externas:
Bien, así que ahora que le hemos dicho al PIC qué pin va a ser para la interrupción, y en qué
flanco se va a disparar la misma, ¿qué ocurre en el programa y en el PIC cuando sucede la
interrupción ?
Ocurren dos cosas. Primero, se activa un flag. Este le dice al procesador interno del PIC que ha
ocurrido una interrupción. Segundo, el Contador de Programa (que hemos mencionado en el
capítulo anterior) apunta a una dirección particular dentro del PIC. Vamos a ver cada cosa
separadamente.
El Flag de Interrupción
Hay un pequeño inconveniente respecto a este flag. Aunque el PIC automáticamente pone
el flag a 1, ¡no lo pone de nuevo a 0! Esta tarea tiene que hacerla el programador (por ejemplo
tú). Se hace muy fácilmente, y como seguro que ya supones, se tiene que hacer después de
que el PIC haya ejecutado la rutina de interrupción.
La Posición de Memoria
Cuando enciendes por primera vez el PIC, o si ocurriese un reset, el Contador de Programa
apunta a la dirección 0000h, que está justo al principio de la memoria de programa. Sin
embargo, cuando ocurre una interrupción, el Contador de Programa apuntará a
la dirección 0004h. Así que, cuando escribamos nuestro programa con interrupciones, lo
primero que tenemos que decirle al PIC es que se salte la dirección 0004h, y mantenga, la
rutina de interrupción que empieza en esa dirección 0004h, separada del resto del programa.
Esto es muy fácil de hacer.
Primero, comenzamos nuestro programa con un comando llamado ORG. Este comando
significa Origen, o inicio. Y lo que le sigue es una dirección.[Nota de la traducción: ORG no es
una instrucción del PIC sino una directiva del ensamblador, al igual que lo son EQU o END que
vimos anteriormente. ORG indica al ensamblador la dirección de memoria a partir de la cual se
deberá ubicar el código que viene escrito a continuación de ella.] Dado que el PIC comienza en
la dirección 0000h, escribimos "ORG 0000h". Lo siguiente que tenemos que hacer es saltarnos
la dirección 0004h. Hacemos esto con una instrucción "GOTO", seguida de la etiqueta que
apunte a nuestro programa principal.
A continuación del "GOTO" ponemos otro comando ORG, pero esta vez seguido de la dirección
0004h. Es después de este comando donde introduciremos la rutina de interrupción. Ahora
después, podríamos o bien escribir directamente nuestra rutina de interrupción seguida de un
segundo comando ORG, o bien podemos poner una instrucción "GOTO" que apunte a la rutina
de interrupción (que podríamos escribirla al final del programa). Realmente es una cuestión de
elegir por tu parte. Después, en la rutina de interrupción, para decirle al PIC que ha llegado al
final de la misma tenemos que poner la instrucción "RETFIE" justo al final. Este comando
significa "retorna de la rutina de interrupción". Cuando el PIC la ve, el Contador de Programa
apuntará a la última posición en la que el PIC estaba antes de que ocurriese la interrupción.
Ponemos aquí un fragmento de código para mostrar lo anterior:
;
ORG 0000h ; El PIC comienza aquí si se enciende o hay
un reset.
GOTO Inicio ; Ve al programa principal.
ORG 0004h ; El PIC vendrá aquí si ocurre una
interrupción.
: ; Esta es nuestra rutina de interrupción
: ; con lo que queremos que haga el PIC
: ; cuando reciba una interrupción.
RETFIE ; Fin de la rutina de interrupción.
Inicio ; Este es el comienzo de nuestro programa
principal
Hay dos cosas que tienes que tener en cuenta cuando utilices interrupciones. La primera que si
estás usando el mismo registro en tu programa principal y en la rutina de interrupción, recuerda
que los contenidos del registro, probablemente cambien cuando ocurra la interrupción. Por
ejemplo, estás utilizando el registro W para enviar datos al puerto A en el programa principal, y
también vas a utilizar el registro W en tu rutina de interrupción para mover datos de una
posición a otra. Si no tienes cuidado, el registro W contendrá el ultimo valor que tenía cuando
estaba en la rutina de interrupción, y cuando vuelvas de la interrupción, este dato se enviará al
puerto A en lugar del valor que tenías antes de que ocurriese la interrupción. Para evitar esto,
dentro de la rutina de interrupción, se tienen que almacenar los contenidos del registro W,
antes de que lo uses . Lo segundo es que tiene que existir un retardo entre que ocurre una
interrupción y que pueda ocurrir la siguiente. Como sabes, el PIC tiene un reloj externo que
puede ser, o bien un Cristal, o bien una red RC (resistencia-condensador). Independientemente
de la frecuencia de reloj, el PIC la divide entre 4 y la utiliza para su reloj interno. Por ejemplo, si
usas un cristal de 4MHz, el PIC llevará a cabo las instrucciones a 1MHz. A este tiempo interno
se le llama Ciclo de Instrucción. Bien, la hoja de características (datasheet), aunque en letra
pequeña, dice que debes dejar que pasen de 3 a 4 ciclos de instrucción entre interrupciones. Mi
consejo es que dejes 4 ciclos. La razón para este retardo es que el PIC necesita tiempo para
saltar a la dirección de interrupción, poner el flag, y volver de la rutina de interrupción. Así que
ten esto en mente si estas utilizando otro circuito que genere una interrupción en el PIC.
Un punto a recordar es que si utilizas los bits del 4 al 7 del puerto B como interrupción, no
puedes seleccionar un pin individual del puerto B para que sirva como interrupción. Así, si
habilitas estos pines, todos estarán habilitados a la vez. De modo que, por ejemplo, no puedes
tener solamente los bits 4 y 5, los bits 6 y 7 también los tendrás habilitados. Entonces ¿ Para
que sirve tener cuatro bits que actúan como interrupción? Bien, podrías tener un circuito
conectado al PIC, y si cualquiera de las cuatro lineas se pusiese a nivel alto, esta podría ser la
condición que necesites para que el PIC actuase rápidamente. Un ejemplo de esto puede ser la
alarma de una casa, donde cuatro sensores estén conectados a los bits 4 a 7 del puerto B.
Cualquier sensor podría activar al PIC para que hiciese sonar una alarma, y que la rutina de
hacer sonar la alarma fuese la rutina de interrupción. Esto ahorra el estar comprobando todo el
tiempo todos los pines, y permite al PIC continuar haciendo otras cosas.
La primera cosa que tenemos que hacer es decirle al PIC que salte la dirección donde el
Contador de Programa apuntará cuando ocurra una interrupción. Aquí notarás que estamos
utilizando una forma distinta de expresar los número hexadecimales. Antes usábamos "F9h",
donde "h" denotaba que era hexadecimal. Podemos escribir esto como 0xF9, y ese será el
formato que vamos a utilizar a partir de ahora.
;
org 0x00 ; Aquí es donde apunta el PC si
energiza el PIC o en caso de reset
goto Principal ; Ir a nuestro programa principal
org 0x04 ; Aquí es donde comienza nuestra
rutina de interrupción.
retfie ; Esto le dice al PIC que la rutina
de interrupción
; ha terminado y el PC volverá a
apuntar al programa principal
Principal ; Este es el comienzo de nuestro
programa principal.
Ahora tenemos que decirle al PIC que vamos a utilizar las interrupciones, y que el pin de
interrupción va a ser el pin 6 (RB0):
;
bsf INTCON,7 ; GIE – Global interrupt enable
(1=habilitado)
bsf INTCON,4 ; INTE - RB0 interrupt enable
(1=habilitado)
Vamos a poner a 0 el flag de interrupción por si acaso (¡¡no nos fiamos de nada!!):
;
bcf INTCON,1 ; INTF - A 0 por si acaso.
Ahora tenemos que configurar nuestros dos puertos. Recuerda que como estamos utilizando
RB0 como pin de interrupción, este debe de ser configurado como entrada:
;
bsf STATUS,5 ; Cambia al banco 1.
movw 0x01 ;
movwf TRISB ; Establece RB0 como entrada
movlw 0x10 ;
movwf TRISA ; Pone los 4 primeros pines del
puerto A como salida
bcf STATUS,5 ; Vuelve al banco 0.
Bucle
Así, nuestro programa principal está escrito, y ahora tenemos que decirle al PIC que hacer
cuando ocurra una interrupción. respecto a esto, nuestra interrupción va a ser una
conmutación. Lo que queremos que haga el PIC es añadir 1 a la variable CONTADOR cada
vez que el switch esté cerrado. Sin embargo, solo queremos mostrar los número que
el switch se cierra de 0 a 9 veces. Antes dijimos que simplemente podríamos incrementar el
puerto A cada vez que hubiese una interrupción. Pero, el puerto A tiene 5 bits, y si simplemente
incrementamos el valor del puerto, tendremos una cuenta de hasta 31. Hay dos razones por las
que elegimos no contar hasta 31. La primera, vamos a utilizar 4 diodos, con los cuales solo
podemos mostrar de 0 a 15 (de 0 a F en hexadecimal, o 0000 a 1111 en binario). Segundo,
también queremos mostrarte algunos comandos aritméticos que has visto en los pasados
capítulos.
;
movwf TEMPORAL ; Almacenamos w en una posición
temporal.
;
incf CONTADOR,1 ; Incrementamos CONTADOR en 1 y
ponemos
; el resultado de vuelta en CONTADOR.
;
movlw 0x0A ; Ponemos 10 en W.
subwf CONTADOR,0 ; Restamos W a CONTADOR y ponemos
; el resultado en W.
;
btfss STATUS,0 ; Comprueba el flag CARRY. Se
activará si
; CONTADOR es igual o mayor que w,
; y se activará como resultado de la
instrucción subwf
Ahora sabemos si el valor de CONTADOR es 9 o más. Lo que queremos hacer ahora es, si
CONTADOR es mayor que 9, ponlo de nuevo a 0, de otro modo vuelve al programa principal
para que podamos enviarlo al PORTA. La instrucción BFTSS, como sabes, se saltará la
siguiente instrucción si el flag de CARRY se pone a 1. Por ejemplo CONTADOR=10:
;
goto continua ; Si CONTADOR es <10, entonces
continua
goto limpiar ; Si CONTADOR es >9, tenemos que
ponerlo a 0
continua
bcf INTCON,0x01 ; Tenemos que poner a 0 este
''flag'' para
; permitir más interrupciones.
movfw TEMPORAL ; Restaura W al valor que tenía
antes de la interrupción.
retfie ; Salir de la rutina de
interrupción.
limpiar
clrf CONTADOR ; Pon CONTADOR otra vez a 0.
bcf INTCON,1 ; Tenemos que poner a 0 este
''flag'' para
; permitir más interrupciones.
retfie ; Salir de la rutina de
interrupción.
Todo lo que queda hacer ahora es ponerlo todo junto y también definir los valores de nuestras
constantes, lo cual se hará justo al principio de nuestro programa.
Aquí debajo está el listado completo. El circuito se muestra después del listado del programa.
Cada vez que pulses el conmutador, los LEDs contarán in binario desde 0000 a 1010, y vuelta
a 0000.
;
org 0x00 ; Aquí es donde apunta el PC si
energiza el PIC o en caso de reset
;
;*****************DEFINICIÓN DE
CONSTANTES********************************
INTCON EQU 0x0B ; Registro de Control de
Interrupciones
PORTB EQU 0x06 ; Dirección del registro PORTB
PORTA EQU 0x05 ; Dirección del registro PORTA
TRISA EQU 0x85 ; Dirección del registro TRISA
TRISB EQU 0x86 ; Dirección del registro TRISB
STATUS EQU 0X03 ; Dirección del registro STATUS
CONTADOR EQU 0x0c ; Esta será nuestra variable
contador
TEMPORAL EQU 0x0d ; Almacén temporal para el registro
W
goto Principal ; Ir a nuestro programa principal
saltando se la dirección de interrupción.
;
;***************RUTINA DE
INTERRUPCION*********************************
org 0x04 ; Aquí es donde comienza nuestra
rutina de interrupción.
movwf TEMPORAL ; Almacenamos w en una posición
temporal.
incf CONTADOR,1 ; Incrementamos CONTADOR en 1 y
ponemos
; el resultado de vuelta en
CONTADOR.
movlw 0x0A ; Ponemos 10 en W.
subwf CONTADOR,0 ; Restamos W a CONTADOR y ponemos
; el resultado en W.
btfss STATUS,0 ; Comprueba el flag CARRY. Se
activará si
; CONTADOR es igual o mayor que w,
; y se activará como resultado de la
instrucción subwf
goto continua ; Si CONTADOR es <10, entonces
continua
goto limpiar ; Si CONTADOR es >9, tenemos que
ponerlo a 0
continua
bcf INTCON,0x01 ; Tenemos que poner a 0 este
''flag'' para
; permitir más interrupciones.
movfw TEMPORAL ; Restaura W al valor que tenía
antes de la interrupción.
retfie ; Salir de la rutina de
interrupción.
limpiar
clrf CONTADOR ; Pon CONTADOR otra vez a 0.
bcf INTCON,1 ; Tenemos que poner a 0 este
''flag'' para
; permitir más interrupciones.
retfie ; Salir de la rutina de
interrupción.
;
;***************PROGRAMA PRINCIPAL***********************************
Principal ; Este es el comienzo de nuestro
programa principal.
;****************Configura los Registros de Interrupción*************
bsf INTCON,7 ; GIE – Global interrupt enable
(1=habilitado)
bsf INTCON,4 ; INTE - RB0 interrupt enable
(1=habilitado)
bcf INTCON,1 ; INTF - A 0 por si acaso.
;
;****************Configura los puertos*******************************
bsf STATUS,5 ; Cambia al banco 1.
movw 0x01 ;
movwf TRISB ; Establece RB0 como entrada
movlw 0x10 ;
movwf TRISA ; Pone los 4 primeros pines del
puerto A como salida
bcf STATUS,5 ; Vuelve al banco 0.
;
;****************Ahora envía el valor de CONTADOR al PORTA***********
Bucle
El Watchdog Timer
Ahora vamos a echar un vistazo a un temporizador interno, llamado "Watchdog Timer" [Nota de
la traducción: En castellano sería, "temporizador perro guardián"]
Supón que has escrito un programa que está continuamente corriendo en un PIC. Ahora,
quieres asegurarte de que el programa sigue ejecutándose siempre, y no hay manera de que
se pare nunca. La primera cosa que tienes que hacer, por supuesto, es un bucle que desde el
final del programa te lleve hasta el principio. Pero ten en cuenta esto. Digamos que el PIC está
monitorizando una entrada. Cuando esta entrada se pone a nivel alto, salta a otra parte del
programa y espera por otro pin para que se ponga a nivel alto. Si el segundo pin no se pone a
nivel alto, el PIC simplemente "se sentará a esperar". Solo saldrá de ahí si el segundo pin se
pone a nivel alto.
Consideremos otro ejemplo. Supón que has escrito un programa, lo has compilado con éxito, e
incluso lo has simulado una y otra vez utilizando un simulador como MPLAB. Todo parece
funcionar bien. Programas el PIC y lo colocas en tu circuito. Sin embargo después de un largo
periodo el programa se atasca en algún punto y el PIC se queda enganchado en un bucle.
Lo que se necesita en ambos casos es alguna clase de reset(o reinicio) si el programa se
quedó atascado. Este es el propósito del watchdog timer.
La primera, cuanto tiempo tenemos antes de tener que hacer un reinicio al WDT.
Segundo, como lo ponemos a cero.
Finalmente, tenemos que decirle al software programador del PIC que habilite el WDT
dentro del PIC.
La hoja de datos del PIC especifica que el WDT tiene un periodo desde su inicio hasta el final
de 18 ms. Esto depende de varios factores, como el voltaje aplicado, la temperatura del PIC,
etc... La razón de esta aproximación es debida a que el reloj del WDT es suministrado por una
red RC interna. El tiempo de carga de la red RC depende del voltaje de alimentación. También
depende de los valores de los componentes, los cuales cambian ligeramente dependiendo de
su temperatura. Así que por razones de simplicidad, tomaremos los 18 ms como el tiempo de
reinicio del WDT.
Sin embargo, podemos hacer este tiempo mayor. Dentro del PIC hay un elemento
llamado Prescaler [Nota de la Traducción: "Prescaler" se puede traducir como "etapa previa de
ajuste de escala"]. Podemos programar este prescaler para dividir el reloj interno de la red RC.
Cuanto mayor sea el factor de división, más tiempo tardará el WDT en reiniciarse.
El prescaler está localizado en el registro OPTION en la dirección 81h, los bit del 0 al 2
inclusive. Más abajo hay una tabla que muestra las asignaciones de los bits para cada ratio de
división y el tiempo de reinicio del WDT:
0 0 0 1:1 18ms
0 0 1 1:2 36ms
0 1 0 1:4 72ms
0 1 1 1:8 144ms
1 0 0 1:16 288ms
1 0 1 1:32 576ms
Recuerda que estos tiempos son independientes de la frecuencia de tu reloj externo. Piensa en
estos tiempos como en tiempo real, en lugar de como tiempos de reloj. Para ayudar a clarificar
esto, vamos a suponer que queremos que el WDT reinicie nuestro PIC después de cerca de
medio segundo como tiempo de seguridad ante fallo. El valor más próximo que tenemos es 576
ms o 0,576 segundos. Todo lo que hacemos es enviar b'101' a nuestro registro OPTION, tal y
como sigue:
La instrucción anterior CLRWDT es la que se utiliza para reiniciar el WDT antes de que este
reinicie al PIC. Así que todo lo que tenemos que hacer es calcular donde en nuestro programa
ocurrirá la finalización del tiempo del WDT, y enviar el comando CLRWDT justo antes de este
punto, para que nos aseguremos de que el PIC no se reinicia. Si tu programa es largo, ten en
cuenta que puede que necesites más de un CLRWDT. Por ejemplo, si utilizas el tiempo por
defecto 18 ms, entonces tenemos que asegurarnos de que el programa ve un CLRWDT cada
18 ms.
Así que ahora llegamos al punto donde tenemos que trabajar en cuanto tiempo tarda nuestro
código en ejecutarse, en tiempo real. El principio es muy simple, pero ¡puede que te tires de los
pelos!
Para aclarar esto, vamos a mirar un código simple, y trabajaremos sobre los ciclos de
instrucción que tarda:
;
movlw 02
movwf CONTADOR
Bucle decfsz CONTADOR
goto Bucle
end
Nuestra primera instrucción simplemente mueve el valor 02 a W. Esto no causa ningún salto,
por tanto es solo 1 ciclo. La siguiente instrucción es similar, mueve los contenidos del registro
W a CONTADOR. De nuevo, esto tardará 1 ciclo. Ahora, la siguiente instrucción primero
decrementa CONTADOR en 1. Esto es 1 ciclo. Después hará una comprobación para ver que
CONTADOR es igual a 0. En este momento no lo es, por tanto vamos a la siguiente instrucción.
La siguiente instrucción es una de "goto", y por tanto tarda 2 ciclos. Volvemos a nuestra
instrucción DECFSZ, la que decrementa CONTADOR en 1 de nuevo. Esto tarda otro ciclo.
Hace una comprobación para ver si CONTADOR es igual a 0. Esta vez lo es, y por tanto se
salta la siguiente instrucción. Para saltarse la siguiente instrucción se requiere otro ciclo.
Alcanzamos el final del programa. Así que en total, con el valor de 02 para CONTADOR, este
programa tarda 7 ciclos en total. Si estamos usando un cristal de 4MHz para nuestro reloj,
entonces el programa tarda:
Software Programador
Dentro del PIC hay elementos llamados "Fusibles" [Nota de Traducción: En ingles "Fuses"]. No
son los mismos que puedes encontrar en los enchufes, sino que son conmutadores
electrónicos que se pueden "fundir" por el programador. Uno de estos fusibles tiene que ser
'fundido' para que el WDT pueda operar. Hay dos formas de hacerlo. Una es escribiendo un par
de lineas al comienzo de tu programa para decirle al software programador del PIC que habilite
o deshabilite ciertos "fusibles". La otra forma de hacerlo es decirle al software programador del
PIC manualmente que fusibles habilitar. Echaremos un vistazo a nuestro programa para instruir
al software programador del capítulo pasado, cuando veamos como incluir otros ficheros y
macros. El cómo hacerlo manualmente varía dependiendo del software de programación. La
documentación que viene con el programador suele decir como hacerlo. Como estoy utilizando
el [software PICALLW], explicaremos como cambiar los "fusibles" con este programa:
Los fusibles se configuran pulsando la tecla F3, o haciendo 'click' sobre el botón de
configuración. Después seleccionas el fusible que quieres habilitado, en ese caso el WDT,
haciendo una marca en la caja que está junto a él.
Programa de ejemplo
Vamos a escribir un programa, donde modifiquemos el WDT, y permitamos al PIC ejecutar una
función. Primero borraremos el WDT periódicamente para mostrar que el programa funciona, y
después quietaremos la instrucción CLRWDT para mostrar que efectivamente el PIC se
reinicia.
Con un reloj de 8KHz, tarda algo menos de 1 segundo en que el siguiente LED se ilumine, y
tarda en total 21 segundos en ir de un extremo al otro y volver (es decir, lo que tarda le rutina
en ejecutarse una vez solamente). El retardo de la subrutina es de 480ms, y la estamos
llamando dos veces antes de mover el bit por los puertos. Ahora, tenemos que hacer el reinicio
periódico del WDT. El mayor tiempo que podemos configurar para el WDT es de 2,3 segundos,
y el siguiente en la tabla es de 1,1 segundos. Tenemos dos opciones. Podríamos hacer una
llamada a la subrutina y reiniciar el WDT después de que los dos Retardos hayan terminado, o
podríamos incorporar el CLRWDT dentro del retardo mismo. Hemos decidido, sin ninguna
razón importante, poner el CLRWDT dentro de la subrutina de retardo.
BUCLE1 ;
BUCLE2 ;
Si comentas o quitas la instrucción CLRWDT, verás que el PIC no pasa de iluminar el segundo
LED. Esto es debido a que el WDT hace que se reinicie el PIC. Con el CLRWDT en su sitio, el
programa funcionará como debe.
Introducción
Programando en CCS.
La programación seria prácticamente imposible sin el uso de variables. Podemos hacernos una
imagen mental de las variables consistente en una caja en la que podemos guardar algo. Esa
caja es una de las muchas que disponemos, y tiene en su frente pegada una etiqueta con su
nombre. Estas cajas tienen ciertas particularidades, que hace que solo se puedan guardar en
ellas determinados tipos de objetos.
Tabla de contenidos
[esconder]
• 1 Introducción
• 2 Tipos
• 3 Declaración
• 4 Asignación de valores
• 5 Varibles Locales y
Globales
• 7 Temas relacionados
• 8 Autor
En esta analogía, cada caja es una variable, su contenido es el valor que adopta, y la etiqueta
es el nombre de la variable. Como su nombre lo indica, y como veremos mas adelante, el
contenido de una variable puede ser modificado a lo largo del programa.
[editar]Tipos
El lenguaje C proporciona cinco tipos básico de datos, con cuatro modificadores posibles.
Podemos utilizar variables de cualquiera de esos tipos. La tabla siguiente muestra los tipos
disponibles:
short 1 0o1
int 8 0 a 255
char 8 0 a 255
unsigned 8 0 a 255
long 16 0 a 65536
Si miras con atención la tabla anterior, puedes ver que hay tipos que parecen estar repetidos.
En realidad, ocurre que CCS permite una "forma corta" para escribir algunos de los tipos.
Concretamente, podemos utilizar unsigned, short, o long en lugar de unsigned int, short int,
o long int.
[editar]Declaración
Las variables deben ser declaradas antes de ser utilizadas en el programa. El proceso de
declaración de variables le dice a CCS de que tipo son y como se llaman. Al igual las demas
instrucciones CCS que veremos a lo largo de este tutorial, debe terminar con ;.
tipo nombre_de_la_variable;
int temperatura;
Esa linea permite a nuestro programa emplear la variable temperatura, que sera capaz de
albergar cualquier valor comprendido entre 0 y 255.
[editar]Asignación de valores
Asignar un valor a una variable es una tarea bien simple. Basta con hacer lo siguiente:
nombre_de_variable = valor;
Por ejemplo, supongamos que queremos asignar el valor "100" a la variable "count". Lo
hacemos de la siguiente manera:
count = 100;
int a = 0;
Hace que la variable a sea del tipo entero, y le asigna el valor 0.
Si la variable es de tipo char, la constante que se le asigna debe estar entre tildes, como en el
siguiente ejemplo:
Por ultimo, tambien podemo asignar a una variable el contenido de otra. En el siguiente
ejemplo, el valor de i sera igual a 10.
int i = 10;
int j;
j = 1;
funcion1 () {
char letra;
.
.
.
. }
En el ejemplo anterior, la variable tipo char llamada letra solo podra utilizarse dentro de
la funcion funcion1(). Si intentamos utilizarla fuera de ella, el compilador nos dará un error.
Si declaramos una variable fuera de cualquier funcion, el alcance de esta sera global, lo que
quiere decir que estará disponible en cualquier parte de nuestro programa. Vemos un ejemplo
de este último caso.
char letra;
main() {
.
.
.
.}
funcion1() {
.
.
.}
La variable tipo char llamada letra podrá utilizarse dentro de main() o de funcion1().
Por ejemplo, el compilador convertirá automaticamente a int cualquier expresión que contenga
variables char, short o int. Esta conversión solo tiene efecto mientras se realizan los cálculos.
Las variables en sí mismas no cambian su tipo.
Las reglas de conversión de tipos hacen que el resultado de una operación sea siempre el
mismo que el de la variable más larga que intervenga en ella.
Sin embargo, podemos forzar a que el resultado sea de un tipo en particular, de la siguiente
forma:
(tipo) valor
donde tipo es el tipo al que queremos que pertenezca valor. El siguiente ejemplo nos aclarará
todo esto:
Tal como explicamos, c no contendrá el valor 2500 como podría paracer a simple vista, por
que el tipo de c no se modica. CCScalcula a * b' y obtiene efectivamente el resultado 2500,
pero c sólo contendrá los 8 bits menos significativos de ese resultado, es decir, el decimal 196.
long c;
c = (long) (a * b);
el valor almacenado en c hubiese sido efectivamente 2500.
Introducción
Programando en CCS.
Llamadas en inglés "preprocessor directives", son comandos que interpreta el primer paso de la
compilacíon que lleva a cabo CCS.
Las directivas más comunes son #define e #include, pero deberías dar un vistazo a todas.
Tabla de contenidos
[esconder]
• 1 Introducción
• 2 #ASM /
#ENDASM
• 3 #BIT
• 4 #BYTE
• 5 #DEFINE
• 6 #DEVICE
• 7 #FUSE
• 8 #INCLUDE
• 9 #INT_xxx
• 10 Temas
relacionados
• 11 Autor
[editar]#ASM / #ENDASM
Este par de instrucciones permite que utilicemos un bloque de instrucciones
en assembler dentro de nuestro código CCS. El siguiente es un ejemplo de uso tomado de la
ayuda del CCS:
Si en lugar de #ASM utilizamos #ASM ASIS, CCS no intentará efectuar cambios de bancos de
memória automaticos para las variables que no pueden ser accedidas desde el banco actual. El
codigo assembler es utilizado "as-is" ("como es").
[editar]#BIT
Permite crear una nueva variable de un bit de tamaño, que es colocada en la memoria
del PIC en la posición del byte x y el bit y. Esto es muy útil para acceder de una manera
sencilla a los registros. Por supuesto, estas variables puedem ser empleadas de la misma
manera que cualquier otra variable tipo short. El formato de #BIT es el siguiente:
[editar]#BYTE
Permite crear una nueva variable de un Byte de tamaño, que es colocada en la memoria
del PIC en la posición del byte x. Esta es una herramienta muy útil para acceder de una
manera sencilla a los registros. Por supuesto, estas variables puedem ser empleadas de la
misma manera que cualquier otra variable tipo int. El formato de #BYTE es el siguiente:
#BYTE nombre = x
#BYTE STATUS = 3
#BYTE PORTB = 6
[editar]#DEFINE
La instrucción #define tiene la siguiente forma:
<label> es la etiqueta que usaremos en nuestro programa. Y value es el valor que estamos
asignando a esta etiqueta. Las instrucciones #DEFINE no generan codigo ASM, si no que el
preprocesador realiza los reemplazos que ellas indican en el momento de la compilación. El
uso de #DEFINE permite construir programas más ordenados y faciles de mantener.
#DEFINE TRUE 1
Cada vez que en nuestro programa aparezca la etiqueta pi, el precompilador la reemplazará
por 3.14159265359
El ejemplo anterior permite una mayor claridad en el programa. Por supuesto, no hay que
abusar de #DEFINE, por que podemos obtener el efecto contrario, haciendo nuestros
programas bastante dificiles de comprender.
Como puedes ver, #DEFINE puede hacer mucho por tus programas.
[editar]#DEVICE
Esta directiva informa al compilador que arquitectura de hardware utilizaremos, para que pueda
generar código apropiado para la cantidad de RAM, ROM y juego de instrucciones disponibles.
Para los chips con más de 256 bytes de RAM se puede seleccionar entre emplear punteros de
8 o 16 bits. Si deseamos emplear punteros de 16 bits basta con añadir *=16 a continuación del
nombre microcontroladorseleccionado.
Veamos algunos ejemplos:
WRITE_EEPROM=ASYNC :
HIGH_INTS=TRUE : Define la prioridad de las interrupciones en los PIC18.
[editar]#FUSE
Permite modificar el valor de los fuses del microcontrolador que estamos empleando. Los
valores posibles dependen de cadamicrocontrolador en particular, y los valores posibles se
cargan al utilizar #ICNLUDE seguido del archivo correspondiente. La forma de#FUSE es la
siguiente:
#FUSE opciones
donde opciones es una lista de las opciones posibles separadas mediante comas. Antes de
seguir, recuerda que puedes ver dentro del archivo con extensión .h correspondiente cuales
son los valores posibles para ese microcontrolador. Están al comienzo del archivo, en forma de
comentarios.
[editar]#INCLUDE
Permite incluir en nuestro programa uno o mas archivos (conocidos como header file) que
posean extensión .h. Estos archivos contienen información sobre funciones, sus argumentos, el
nombre de los pines de un modelo determinado de PIC o cualquier otra cosa que usemos
habitualmente en nuestros programas. Esto permite no tener que escribir un montón de cosas
cada vez que comenzamos un programa nuevo: basta con incluir el .h correspondiente.
#INCLUDE <archivo>
Esto hará que el contenido de <archivo> se compile junto con nuestro programa. Por ejemplo:
#INCLUDE <PIC16F877A.H>
hace que todas las especificaciones de nombres y registros del PIC16F877A se incluyan en
nuestro programa. Esto permitirá referirnos al pin 0 del PORTB del PIC mediante PIN_B0.
[editar]#INT_xxx
#INT_xxxindica que la función que le sigue (en el código fuente CCS) es una función de
interrupción. Estas funciones no deben tener parámetros. Por supuesto, no todos
los PICs soportan todas las directivas disponibles:
Ejemplo:
#int_ad
adc_handler() {
adc_active=FALSE;
}
#int_rtcc noclear //"noclear" evita que se borre el flag
correspondiente.
isr() {
...
}
En CCS los operadores cumplen un rol importante. Quizas C sea uno de los lenguajes que
mas operadores tiene. Una expresión es una combinacion deoperadores y operandos. En la
mayoría de los casos, los operadores de CCSsiguen las mismas reglas que en álgebra, y se
llaman de la misma manera.
Tabla de contenidos
[esconder]
• 1 Introducción
• 2 Operadores aritméticos
o 2.1 Atajos
• 3 Operadores Relacionales
• 4 Operadores Lógicos
• 5 Operadores de bits
o 5.1 Atajos
• 6 Otros operadores
• 7 Precedencia de los
operadores
• 8 Temas relacionados
• 9 Autor
+ (suma)
- (substracción)
* (multiplicación)
/ (división)
% (módulo)
Los primeros cuatro operadores mencionados se pueden utilizar con cualquier tipo de dato.
Estos son algunos ejemplo de como usarlos:
a = b + c;
a = b - c;
a = b * c;
a = b / c;
a = -a; //Cambia el signo de "a".
a = a + 1; //suma 1 al valor de "a".
El operador % (módulo) solo puede emplearse con enteros. Devuelve el resto de una división
de enteros. Veamos un par de ejemplos:
int a = 10, b = 5, c;
c = a % b; //"c" valdrá cero.
int a = 20, b = 3, c;
c = a % b; //"c" valdrá 2.
[editar]Atajos
CCS también provee atajos para utilizar los operadores aritméticos. Hay algunas operaciones
que se repiten a menudo cuando creamos nuestros programas, y estos atajos ayudan a que
podamos escribir nuestro código más rapidamente. Los atajos provistos son los siguientes.
a *= b es lo mismo que a = a * b
a /= b es lo mismo que a = a / b
a += b es lo mismo que a = a + b
a -= b es lo mismo que a = a - b
a %= b es lo mismo que a = a * b
a = b && ( q || n )
Y se pueden comninar con los demas operadores vistos:
& (AND)
| (OR)
^ (XOR)
~ (complemento)
<< (desplazamiento a la izquierda)
>> (desplazamiento a la derecha)
a&b=8
a | b = 125
a ^ b = 117
~ a = 135
a = 11111000
b = 00001101
luego
Si a era igual a 120 ( 01111000 en binario) pasará a valer 192 (11000000 en binario).
[editar]Atajos
CCS también provee atajos para utilizar los operadores de bits. Hay algunas operaciones que
se repiten a menudo cuando creamos nuestros programas, y estos atajos ayudan a que
podamos escribir nuestro código más rapidamente. Los atajos provistos son los siguientes.
++ Operador incremento
-- Operador decremento
Estos operadores permiten sumar (o restar) uno al valor de una variable. Lo que generalmente
hariamos asi:
a = a + 1
0 asi:
a = a - 1
o asi:
a--
el resultado sera el mismo, pero es más corto de escribir, y más fácil de utilizar en expresiones
complejas.
()
signo +, signo -, ++, --, !, (<tipo>)
*, /, %
+, -
<, <=, >, >=
==, !=
&&, ||
=, +=, -=, *=, /=, %=
CCS - Punteros
[editar] Introducción
Programando en CCS.
Una de las caracteristicas mas interesantes de las diferentes versiones de Cson los punteros. Por supuesto, CCS permite el mane
lo que nuestros progamas pueden aprovechar toda la potencia de esta herramienta.
El presente artículo fue escrito por Pedro (PalitroqueZ), un amigo de uControl. Su dirección de correo elecrónico es palitroquez
Tabla de contenidos
[esconder]
• 1 Introducción
• 2 ¿Qué es un puntero?
variable?
• 8 Punteros en funciones
• 9 Punteros en Arrays
• 10 Temas relacionados
• 11 Autor
Conociendo la dirección del registro o variable y pudiéndolo manejar nos da un poderosa herramienta para agilizar/simplificar n
Ejemplo1:
#include <18F4550.h>
#use delay(clock=4000000)
void main(){
int t,k;
t=5;
k= &t;
delay_cycles(1);
}
Cuando detenemos en delay_cycles(1) vemos que en k se guarda la dirección de la variable t, ¿y que guarda t? guarda el número
usando memoria RAM ó el registro de propósito general GPR.
se repite lo mismo, el resultado de las direcciones en k, l y m son contiguas. Pero... ¿por que?
Para responder esta pregunta vamos a cambiar el código otra vez, declarando los 3 tipos de registros conocidos, int, long y float
#include <18F4550.h>
#use delay(clock=4000000)
void main(){
int k,l,m,n;
int t;
long u;
float v;
int z;
t=0xfa; z=0xff; u=0xfffa; v=3.45000000;
k= &t; l= &u; m= &v; n=&z;
delay_cycles(1);
}
la simulación:
Observa que las direcciones de t, u y v saltan. ¿Por que?
Dependiendo del tipo de dato se consume >= 1 byte de memoria. En el caso de t es un entero, y los enteros ocupan 1 byte (0..25
"entero largo", ocupa dos bytes (0..65535) v es un dato "coma flotante", con parte fraccionaria en el sistema decimal y toma 4 by
bits)
Esto quiere decir que se le debe pasar el número por valor de la dirección de la variable normal. Recordemos que:
#include <18F4550.h>
#use delay(clock=4000000)
//*******************************
void main(){
int k; // variable normal
int *p; // la variable puntero
k=0xfa; // k <- 0xfa
*p=0x5;
delay_cycles(1);
}
Dentro del código reconocemos de inmediato quien es el puntero: el que tiene el símbolo * debe ir antes de la letra p y sin separa
Es simple: porque no fijamos una dirección que apuntara p, y esto es muy importante saberlo, era lo que se decía al inicio de este
darle la dirección de k:
#include <18F4550.h>
#use delay(clock=4000000)
//*******************************
void main(){
int k; // variable normal
int *p; // la variable puntero
p=&k; // dirección de k copiada a p
k=0xfa; // k <- 0xfa
*p=0x5; // k <- 0x5
delay_cycles(1);
}
el resultado:
Ahora si funciona correctamente el código. Si ven la línea:
Podran ovservar que se usa el puntero sin el *. Esto significa que se guardará allí una dirección y el compilador lo interpreta de e
Otro detalle a tomar en cuenta es que para apuntar cierto tipos de datos, es que se debe declarar al apuntador con el mismo tipo d
no quiere decir que el tipo de datos que contendrá el puntero sea de ese tipo de datos, el puntero siempre soportará números ente
realidad esto ya es a nivel interno del compilador.
Un ejemplo más:
#include <18F4550.h>
#use delay(clock=4000000)
//*******************************
void main(){
int i; // variable normal
int *p; // la variable puntero
int j;
int *q;
int k;
//
p=&i; // dirección de i copiada a p
q=&j;
//
i=0xfa; // i <- 0xfa
j=0x11;
k=0x22;
//
*p=0x5; // i <- 0x5
*q=0x33;
delay_cycles(1);
}
#include <18F4550.h>
#use delay(clock=4000000)
//*******************************
void main(){
float i; // variable normal
float *p; // la variable puntero
//
long j;
long *q;
int k;
//
i=2.51; // i <- 0xfa
j=0x11;
k=0x22;
//
p=&i; // dirección de i copiada a p
q=&j;
//
*p=3.99; // i <- 0x5
*q=0x33;
delay_cycles(1);
}
Entre i, p hay 4 bytes -> i ocupa 4 bytes.
Entre p, j hay 2 bytes -> puntero p ocupa 2 bytes.
Entre j, q hay 2 bytes -> j ocupa 2 bytes.
Entre q, k hay 2 bytes -> puntero q ocupa 2 bytes.
En ambos casos a pesar que cambiamos el tipo de declaración de los punteros, se mantienen en 2 bytes, eso quiere decir que para
tamaño de un puntero es de 2 bytes. No confundir con el tipo de datos a direccionar, pues eso es otra cosa.
Vamos con otro ejemplo. Supongamos que i sea del tipo float (4 bytes) pero su apuntador lo declaramos como int (1 byte):
#include <18F4550.h>
#use delay(clock=4000000)
//*******************************
void main(){
float i; // variable normal
int *p; // la variable puntero
long j;
long *q;
int k;
//
i=2.51; // i <- 0xfa
j=0x11;
k=0x22;
//
p=&i; // dirección de i copiada a p
q=&j;
//
*p=3.99; // i <- 0x5
*q=0x33;
delay_cycles(1);
}
Noten que el nuevo valor de i no corresponde con el valor que indirectamente le dimos con el apuntador.
¿Por que? Porque declaramos a ese apuntador como entero (int) y con ello le estamos diciendo al compilador que reserve para p
en vez de 4 bytes que son los que se necesitan y por eso ocurre ese truncamiento y da ese valor extraño.
#include <18F4550.h>
#use delay(clock=4000000)
//*******************************
void main(){
float i; // variable normal
float *p; // la variable puntero
long j;
long *q;
int k;
//
i=2.51; // i <- 0xfa
j=0x11;
k=0x22;
//
p=&i; // dirección de i copiada a p
q=&j;
//
*p=3.99; // i <- 0x5
*q=0x33;
delay_cycles(1);
}
con 4 modos de selección: CCS2,CCS3,CCS4 y ANSI. Con CCS2 y CCS3 el tamaño (size) del puntero es de 1 byte en partes d
con CCS4 (modo por defecto) el size es de 2 bytes.
Analizando nuevamente lo hablado referente al size de los punteros en CCS, y en un intento de explicar que el tipo de dato y el t
son 2 cosas distintas, vamos hacer un ejemplo donde se verá claramente. Para ello vamos a usar una directiva llamada#locate, so
del compilador reza así:
#LOCATE works like #BYTE however in addition it prevents C from using the area
bueno esto quiere decir que la variable normal la puedo alojar en cualquier dirección de la RAM (dentro de ciertos limites). Algo
ensamblador pusieramos:
#include <18F4550.h>
#use delay(clock=4000000)
//*********************************
int dato=0xaa; // declaramos dato (GPR) y lo cargamos con 0xAA
#locate dato = 0xff
// le decimos al compilador que dato estará en la dirección 0xFF
//del área de registro de propósito general, traducido, en la RAM del PIC
void main(){
int *p; // declaramos un puntero como entero (igual que dato)
int t; // otra variable normal
p=&dato; // inicializamos al puntero
*p=0xbb; // dato <- 0xBB
delay_cycles(1); // un nop
}
Fíjense que el puntero p ocupa 2 bytes a pesar que está declarado como int (1 byte).
Vamos a modificar este ejemplo pero usando float (4 bytes) y colocando el GPR en la dirección 0xAF
Observen que el puntero p se mantuvo en 2 bytes siendo éste declarado como float.
Supongamos un ejemplo para el PIC18F4550, en el que tenemos una memoria de datos que llega hasta 0x7FF (Pág. 66 de su hoj
funcione 0x7FF debe ser el 4 byte para un float, entonces
float dato=1.23456789;
#locate dato = 0x7FB
...
Si que funcionó. Pero, ¿que pasa si asignamos el dato a 0x800?
Allí vemos que el puntero se cargó bien, pero el MPLAB-SIM delata el desbordamiento, ¿Por que? Es que a partir de allí no hay
las direcciones se deberían leer como puros 0x0 a pesar que compiló bien, (similarmente en programas de computadoras pueden
infinitos popularmente llamado ‘se colgó la máquina’)
Punteros en funciones
[editar]
Todo lo que hagamos en CCS se hace a través de funciones o procedimientos, desde el punto de vista matemático una función se
Una función es una relación entre dos variables numéricas, habitualmente las denominamos x e y; a una de ellas la llamamos v
pues depende de los valores de la otra para su valor, suele ser la y; a la otra por tanto se la denomina variable independiente y
además, para que una relación sea función, a cada valor de la variable independiente le corresponde uno o ningún valor de la var
le pueden corresponder dos o más valores.
Aplicándolo a la programación, significa que podemos tener varios argumentos o parámetros de entrada, pero solo tendremos un
eso no es todo, en C una función pasa los argumentos por valor. ¿que quiere decir esto? Que cuando llamemos a la función y le p
como argumento, ésta copiará ese dato en su propia función sin alterar la variable original. veamos un ejemplo:
#include <18F4550.h>
#use delay(clock=4000000)
//*********************************
int mi_funcion(int argumento1, argumento2){
delay_cycles(1);
return (argumento1 + argumento2);
}
//*******************************
void main(){
int k,l,resultado;
k=5; L=2;
resultado = mi_funcion(k,L);
delay_cycles(1);
}
Noten que cuando llamo a mi_funcion, se copia el contenido de k -> argumento1 y L -> argumento2 , luego hace la suma y re
resultado de la suma. k y L se quedan con el mismo valor anterior.
Bueno seguro que alguien llegará y colocará a k y L como globales y entonces así se puede modificar en cualquier lado. Pero si
dentro de main(), no se puede modificar fuera de main()...a menos que usemos punteros. ¿y como se haría eso?
Simple: se haría pasando el argumento a la función como referencia, haciendo referencia a la dirección, es decir lo que se pasará
dirección de k, L entonces allí si se puede modificar a gusto. Un ejemplo:
#include <18F4550.h>
#use delay(clock=4000000)
//*********************************
int mi_funcion(int argumento1, argumento2, *la_k, *la_L){
delay_cycles(1);
*la_k=0xFF; *la_L=0xAF;
return (argumento1 + argumento2);
}
//*******************************
void main(){
int k,l,resultado;
k=5; l=2;
resultado = mi_funcion(k,l,&k,&l);
delay_cycles(1);
}
Punteros en Arrays
[editar]
Como sabrán los arrays son arreglos de datos que van en direcciones consecutivas, es decir, uno detrás del otro. Un ejemplo de
char cadena[7]={'T','o','d','o','P','i','c'};
Si lo probamos en un código:
#include <18F4550.h>
#use delay(clock=4000000)
//*********************************
char cadena[7]={'T','o','d','o','P','i','c'};
void main(){
char c;
int t;
for(t=0;t<7;t++){
c=cadena[t];
}
delay_cycles(1);
}
Se pueden usar punteros en el ejemplo anterior. Veamos como:
char c, *p;
lo inicializamos (le damos la dirección del primer elemento del array):
p=&cadena[0];
luego hacemos un barrido de direcciones para tomar el contenido de cada elemento y guardarlo en c
for(t=0;t<7;t++){
c= *p + t;
}
El primer error es que según la precedencia del operador primero está el puntero y luego viene la suma, y así estaríamos sumand
varían, la solución es usar *(p+i)
¿Y que es eso de que varían? Pues que cuando se recorre el arrays con el puntero, este debe ir sumando direcciones, pero direcci
constantes, es decir, si el tipo de datos es 1 byte, entonces el puntero debe acumular números enteros de 1 byte en 1 byte
Si el tipo de datos es long (entero largo) entonces el puntero debe ir sumando direcciones de 2 bytes en 2 bytes. ¿Porque digo est
fijo (la dirección) y el truco está en desplazar al puntero tantas posiciones sea el size del tipo de dato. Este sería el segundo error
misma: *(p+i)
Vamos a cambiar ese ejemplo por números long para que se entienda
#include <18F4550.h>
#use delay(clock=4000000)
//*********************************
long cadena[7]={1000,2000,3000,4000,5000,6000,7000};
void main(){
long c, *p;
int t;
p=&cadena[0];
for(t=0;t<7;t++){
c= *p + t;
}
delay_cycles(1);
}
Fíjense que p queda inmutable, y lo que hace el programa es contenido[0] + t. ¡Grave error!
Con esto estamos garantizando que el puntero se moverá de 2 bytes en 2 bytes, es decir
*(p+t) =
Noten que la suma se realiza no intervalos de t sino en intervalos del ancho del tipo de dato. Pero...¿esto no es lo mismo que se h
inicio del artículo?
Pues si, acabamos de ver un array al desnudo, como funciona en realidad, no es mas que un puntero escondido a nuestra vista. So
fácil la programación el compilador lo acepta de esta manera. Si p es un puntero -> p = cadena (para el primer índice del arreglo
válido, se acepta que cadena es un puntero constante, también se podría llamar un puntero nulo (ya que no se ve y tampoco se pu
Ejemplos validos:
cadena[0] = *cadena
cadena[2] = *(cadena + 2)
cadena = *(cadena + i)
#include <18F4550.h>
#use delay(clock=4000000)
//*********************************
char cadena[7]={'T','o','d','o','P','i','c'};
void main(){
char c, *p;
int t;
p=cadena;
for(t=0;t<7;t++){
c=*(p+t);
}
delay_cycles(1);
}
CCS - Funciones
Introducción
[editar]
Programando en CCS.
Las funciones son los bloques básicos con los que construimos un programa enCCS. Además de la funcion main() que veremos
programa CCStendrá seguramente varias funciones más, conteniendo cada una un bloque de instrucciones que realizan una tarea
Tabla de contenidos
[mostrar]
Funciones
[editar]
nombre_de_la_funcion() {
instruccion;
instruccion;
.
.
instruccion; }
Para evitar que surjan errores o avisos (warnings) al compilar nuestros programas, debemos declarar las funciones antes de utiliz
Prototipos
[editar]
Existen dos formas de decirle al compilador CCS que tipo de valor devolverá nuestra función. La forma general es la siguiente:
tipo nombre_de_funcion();
donde tipo es cualquiera de los tipos de variables soportados por CCS. Al igual que cualquier instrucción de CCS, la linea debe
y coma).
El siguiente ejemplo declara la funcion ejemplo() que devuelve como resultado un valor del tipo long:
long ejemplo();
[editar]Parámetros
Ademas de determinar el tipo de resultado que devolverá la función, en el prototipo podemos especificar que parametros recibirá
La forma de hacerlo es la siguiente:
tipo nombre_de_funcion(tipo var1, tipo var2, ..., tipo varN);
La diferencia con el caso anterior es que se han incluido dentro de los () una serie de nombres de variables (var1, var2, ..., varN)
un tipo en particular.
Supongamos que queremos crear una función que lleve a cabo la suma de dos de tipo int, que le son pasados como argumentos,
resultado en formato double. Deberiamos escribir así su prototipo:
donde a y b son los valores a sumar. El llamado a la función se puede hacer de la siguiente manera:
int a, b;
double resultado;
a = 10;
b = 250;
resultado = suma (a, b);
Return
[editar]
La forma en que se asigna en la función el valor que esta debe devolver es mediante la instrucción return.
Vemoslo con el ejemplo de la función suma vista mas arriba. La función podría ser como sigue:
Void
[editar]
void significa que la función no devolverá ningún parametro. Supongamos que la función ejemplo() no debe regresar ningún va
llamada. Su prototipo debería ser como sigue:
void ejemplo();
Además, podemos usar void para para indicar que la función no recibe parámetros:
void ejemplo2(void);
La función main()
[editar]
Como hemos visto, el lenguaje C permite la utilización de funciones. Pero hay una función especial, llamada main() que obliga
presente, y es el punto de entrada a todo programa en C que escribamos.
main() {
instruccion;
instruccion;
.
.
instruccion; }
donde instruccion; puede ser cualquier instrucción válida del CCS o una llamada a otra función.
Programando en CCS.
En CCS no disponemos de instrucciones específicas para el manejo de pantallas LCD. Sin embargo, nada impide que escribamo
capaces de inicializar, escribir o borrar (e incluso leer) los datos de estas pantallas. El hecho de que la mayoría de los módulos
construidos en base al controlador Hitachi HD44780.
Tabla de contenidos
[mostrar]
LCD.C
[editar]
Para ponernos las cosas más faciles, dentro de la carpeta "drivers" de CCS se encuentra un archivo llamado LCD.C, que si lo in
proyecto, nos proveerá de las funciones necesarias. Sin embargo, LCD.C tiene algunas limitaciones: tal como está, solo funcion
nuestro LCD en el puerto D (o B, con una modificación menor).
#INCLUDE "lcd.c"
como puede verse, se trata de una comunicación con solo 4 bits de datos. Más adelante veremos como modificar este archivo par
emplear con el LCD en otro puerto y/o con otra asignación de pines. El siguiente es el contenido del archivo LCD.C tal como es
///////////////////////////////////////////////////////////////////////////
//// LCD.C ////
//// Driver for common LCD modules ////
//// ////
//// lcd_init() Must be called before any other function. ////
//// ////
//// lcd_putc(c) Will display c on the next position of the LCD. ////
//// The following have special meaning: ////
//// \f Clear display ////
//// \n Go to start of second line ////
//// \b Move back one position ////
//// ////
//// lcd_gotoxy(x,y) Set write position on LCD (upper left is 1,1) ////
//// ////
//// lcd_getc(x,y) Returns character at position x,y on LCD ////
//// ////
///////////////////////////////////////////////////////////////////////////
//// (C) Copyright 1996,2003 Custom Computer Services ////
//// This source code may only be used by licensed users of the CCS C ////
//// compiler. This source code may only be distributed to other ////
//// licensed users of the CCS C compiler. No other use, reproduction ////
//// or distribution is permitted without written permission. ////
//// Derivative programs created using this software in object code ////
//// form are not restricted in any way. ////
///////////////////////////////////////////////////////////////////////////
//
// As defined in the following structure the pin connection is as follows:
// D0 enable
// D1 rs
// D2 rw
// D4 D4
// D5 D5
// D6 D6
// D7 D7
//
// LCD pins D0-D3 are not used and PIC D3 is not used.
//
// Un-comment the following define to use port B
// #define use_portb_lcd TRUE
//
//
struct lcd_pin_map { // This structure is overlayed
BOOLEAN enable; // on to an I/O port to gain
BOOLEAN rs; // access to the LCD pins.
BOOLEAN rw; // The bits are allocated from
BOOLEAN unused; // low order up. ENABLE will
int data : 4; // be pin B0.
} lcd;
//
#if defined(__PCH__)
#if defined use_portb_lcd
#byte lcd = 0xF81 // This puts the entire structure
#else
#byte lcd = 0xF83 // This puts the entire structure
#endif
#else
#if defined use_portb_lcd
#byte lcd = 6 // on to port B (at address 6)
#else
#byte lcd = 8 // on to port D (at address 8)
#endif
#endif
//
#if defined use_portb_lcd
#define set_tris_lcd(x) set_tris_b(x)
#else
#define set_tris_lcd(x) set_tris_d(x)
#endif
//
#define lcd_type 2 // 0=5x7, 1=5x10, 2=2 lines
#define lcd_line_two 0x40 // LCD RAM address for the second line
//
BYTE const LCD_INIT_STRING[4] = {0x20 | (lcd_type << 2), 0xc, 1, 6};
// These bytes need to be sent to the LCD
// to start it up.
//
// The following are used for setting
// the I/O port direction register.
struct lcd_pin_map const LCD_WRITE = {0,0,0,0,0}; // For write mode all pins are out
struct lcd_pin_map const LCD_READ = {0,0,0,0,15}; // For read mode data pins are in
//
BYTE lcd_read_byte() {
BYTE low,high;
set_tris_lcd(LCD_READ);
lcd.rw = 1;
delay_cycles(1);
lcd.enable = 1;
delay_cycles(1);
high = lcd.data;
lcd.enable = 0;
delay_cycles(1);
lcd.enable = 1;
delay_us(1);
low = lcd.data;
lcd.enable = 0;
set_tris_lcd(LCD_WRITE);
return( (high<<4) | low);
}
//
void lcd_send_nibble( BYTE n ) {
lcd.data = n;
delay_cycles(1);
lcd.enable = 1;
delay_us(2);
lcd.enable = 0;
}
//
void lcd_send_byte( BYTE address, BYTE n ) {
lcd.rs = 0;
while ( bit_test(lcd_read_byte(),7) ) ;
lcd.rs = address;
delay_cycles(1);
lcd.rw = 0;
delay_cycles(1);
lcd.enable = 0;
lcd_send_nibble(n >> 4);
lcd_send_nibble(n & 0xf);
}
//
void lcd_init() {
BYTE i;
set_tris_lcd(LCD_WRITE);
lcd.rs = 0;
lcd.rw = 0;
lcd.enable = 0;
delay_ms(15);
for(i=1;i<=3;++i) {
lcd_send_nibble(3);
delay_ms(5);
}
lcd_send_nibble(2);
for(i=0;i<=3;++i)
lcd_send_byte(0,LCD_INIT_STRING[i]);
}
//
void lcd_gotoxy( BYTE x, BYTE y) {
BYTE address;
if(y!=1)
address=lcd_line_two;
else
address=0;
address+=x-1;
lcd_send_byte(0,0x80|address);
}
//
void lcd_putc( char c) {
switch (c) {
case '\f' : lcd_send_byte(0,1);
delay_ms(2);
break;
case '\n' : lcd_gotoxy(1,2); break;
case '\b' : lcd_send_byte(0,0x10); break;
default : lcd_send_byte(1,c); break;
}
}
//
char lcd_getc( BYTE x, BYTE y) {
char value;
lcd_gotoxy(x,y);
while ( bit_test(lcd_read_byte(),7) ); // wait until busy flag is low
lcd.rs=1;
value = lcd_read_byte();
lcd.rs=0;
return(value);
}
[editar]Funciones en LCD.C
Hay cuatro funciones implementadas dentro de LCD.C:
[editar]lcd_init()
Esta funcion es la encargada de enviar los comando de inicializacion necesarios al LCD. Es obligatorio ejecutar esta función ant
display para escribir sobre él. No recibe ni devuelve valores de ningun tipo. Su forma de uso es tan simple como:
lcd_init();
y listo.
[editar]lcd_putc()
Esta seguramente será la función que mas emplearemos. Es la que se encarga de escribir nuestro mensaje en la pantalla. No devu
(obviamente) los recibe. La forma de uso es muy simple. Básta con llamarla, pansandole como parámetro una variable o constan
función se encargará de desplegar su contenido sobre el display.
Lcd_putc ("uControl.com.ar");
Por supuesto, no debemos olvidar de inicializar previamente el display. Además, lcd_putc() reconoce los siguientes comandos qu
enviados en el texto a mostrar:
Esto quiere decir que si modificamos nuestro código para que quede así:
Lcd_putc ("\f");
Esta es la función que nos permite colocar el cursor en la parte que deseemos de la pantalla. Recibe dos parámetros, ambos de tip
ellos indica la columna en la que aparecerá el primer caracter del texto, y el segundo se refiere a la fila en que lo hará. El siguien
ejemplifica el uso de lcd_gotoxy(x,y):
Lcd_putc ("uControl.com.ar");
lcd_gotoxy(5,2); //salto a columna 4, fila 2
Lcd_putc( "LCD en CCS");
hace lo siguiente:
[editar]lcd_getc(x,y)
Esta función recibe como parámetros la columna' y la fila (ambos de tipo byte) de la que deseamos conocer el contenido, y nos
un char con el contenido. Su uso no podría ser más sencillo:
char a;
a = lcd_getc(5,2);
Modificando LCD.C
[editar]
Por supuesto, en la mayoría de los casos la conexión entre el microcontrolador y el display LCD no coincidirá con la especificad
archivo LCD.C provisto por CCS. Pero eso no quiere decir que debamos rediseñar nuestro proyecto, ni que sea imposible modif
de LCD.C. como puede verse, dentro del codigo de LCD.C se define una estructura de datos que es la encargada de contener la
pines a utilizar, junto a la función que desempeñará. Es el siguiente trozo de código:
Notar que no estámos usando el pin RW del display, que estará permanentemente conectado a GND. La estructura deberia qued
struct lcd_pin_map {
BOOLEAN unused1; // RB0
BOOLEAN unused2; // RB1
BOOLEAN enable; // RB2
BOOLEAN rs; // RB3
int data : 4; // RB4-RB7
} lcd;
Por supuesto, habrás notado que en lugar del puerto D estamos usando el puerto B, asi que hay que quitar el comentario a la line
Y como no estamos usando la línea RW del LCD, debemos quitar las dos tres lineas de código en la que se hace referencia a ella
corresponde al archivo LCD.C con todas las modificaciones mencionadas, más algunas modificaciones en los#INCLUDE del p
tienen sentido mantener ya que al personalizar el archivo nunca se van a dar algunas de las condiciones contempladas alli. Tamb
código de la función lcd_getc( x, y) ya que al estar RW conectado de forma permanente aGND, será imposible leer caracteres d
///////////////////////////////////////////////////////////////////////////
// LCD.C modificada por uControl.com.ar
///////////////////////////////////////////////////////////////////////////
// B0
// B1
// B2 E
// B3 RS
// B4 D4
// B5 D5
// B6 D6
// B7 D7
// (Sin 'RW')
//
// Funciones soportadas:
// lcd_init()
// lcd_gotoxy( BYTE col, BYTE fila)
// lcd_putc( char c)
// \f Clear display
// \n Go to start of second line
// \b Move back one position
//
///////////////////////////////////////////////////////////////////////////
#define use_portb_lcd TRUE //LCD conectado al puerto b.
//
struct lcd_pin_map {
BOOLEAN unused1; // RB0
BOOLEAN unused2; // RB1
BOOLEAN enable; // RB2
BOOLEAN rs; // RB3
int data : 4; // RB4-RB7
} lcd;
//
#byte lcd = 0xF81 // Direccion de la estructura "lcd".
#byte lcd = 6 // Direccion del puerto B.
#define set_tris_lcd(x) set_tris_b(x)
#define lcd_type 2 // Tipo de LCD: 0=5x7, 1=5x10, 2=2 lineas
#define lcd_line_two 0x40 // Dirección de la LCD RAM para la 2da. linea
//
//Defino la cadena de inicializacion del LCD.
BYTE const LCD_INIT_STRING[4] = {0x20 | (lcd_type << 2), 0xc, 1, 6};
//
//Configuro el estado de cada pin para lectura y escritura:
struct lcd_pin_map const LCD_WRITE = {0,0,0,0,0}; // Escribir.
struct lcd_pin_map const LCD_READ = {0,0,0,0,15}; // Leer.
//
//Funciones:
BYTE lcd_read_byte() {
BYTE low,high;
set_tris_lcd(LCD_READ);
delay_cycles(1);
lcd.enable = 1;
delay_cycles(1);
high = lcd.data;
lcd.enable = 0;
delay_cycles(1);
lcd.enable = 1;
delay_us(1);
low = lcd.data;
lcd.enable = 0;
set_tris_lcd(LCD_WRITE);
return( (high<<4) | low);
}
//
void lcd_send_nibble( BYTE n ) {
lcd.data = n;
delay_cycles(1);
lcd.enable = 1;
delay_us(2);
lcd.enable = 0;
}
//
void lcd_send_byte( BYTE address, BYTE n ) {
lcd.rs = 0;
while ( bit_test(lcd_read_byte(),7) ) ;
lcd.rs = address;
delay_cycles(1);
delay_cycles(1);
lcd.enable = 0;
lcd_send_nibble(n >> 4);
lcd_send_nibble(n & 0xf);
}
//
void lcd_init() {
BYTE i;
set_tris_lcd(LCD_WRITE);
lcd.rs = 0;
lcd.enable = 0;
delay_ms(15);
for(i=1;i<=3;++i) {
lcd_send_nibble(3);
delay_ms(5);
}
lcd_send_nibble(2);
for(i=0;i<=3;++i)
lcd_send_byte(0,LCD_INIT_STRING[i]);
}
//
void lcd_gotoxy( BYTE x, BYTE y) {
BYTE address;
if(y!=1)
address=lcd_line_two;
else
address=0;
address+=x-1;
lcd_send_byte(0,0x80|address);
}
//
void lcd_putc( char c) {
switch (c) {
case '\f' : lcd_send_byte(0,1);
delay_ms(2);
break;
case '\n' : lcd_gotoxy(1,2); break;
case '\b' : lcd_send_byte(0,0x10); break;
default : lcd_send_byte(1,c); break;
}
}
Display GLCD
El compilador CCS proporciona una libreria capaz de dibujar primitivas sobre varios modelos de displays LCD gráficos o GLCD
Liquid Cristal Display). Hay versiones de esta libreria para pantallas con diferentes controladores embebidos, como el Samsung
el Toshiba T6963.
Pero a pesar de que pueden distribuirse libremente los trabajos que hagamos con ellas, no pueden compartirse los programas que
menos que la persona que los recibe tambien sea un usuario registrado de CCS.
Esto limita mucho su uso con fines educativos. De hecho, si quisiesemos exponer aqui un programa que grafique algo en un GLC
violando la licencia, ya que es muy posible que muchos de los lectores de uControl no hayan comprado el compilador.
Es por ello que nos hemos decidido a escribir una libreria propia, que usaremos de ahora en más para nuestros proyectos.
Tabla de contenidos
[mostrar]
La librería GLCD_K0108
[editar]
IMPORTANTE: El trazado de lineas se basa en el Algoritmo de Bresenham, y las circunferencias se han resuelto mediante el"
medio", que divide la circunferencia en 8 partes simétricas, evitando utilizar funciones como seno, coseno o potencias, que volv
tarea del trazado.
[editar]GLCD_limpiar(color)
Esta es la función que "pinta" toda la pantalla con uno u otro color. Si recibe como parámetro un "1", la pintará completamente d
"0", la limpiará por completo.
Su funcionamiento también es muy sencillo, y se "apoya" en GLCD_envia BYTE() para escribir en el GLCD. Recorre ambas mit
página por página, de arriba hacia abajo, escribiendo "0x00" o "0xFF" según se haya elegido pintar o borrar.
En la primer imágen, utilizando GLCD_limpiar(1);, la pantalla se pinta completamente de negro. En la segunda, medianteGLC
pinta completamente de blanco. Podemos usar esta función para limpiar la pantalla.
Se encarga de inicializar el GLCD, y el parametro "modo" determina si estará encendido (si recibe un "1") o apagado (si recibe u
[editar]GLCD_punto(x, y, color)
Esta es la "primitiva gráfica" indispensable. A partir de GLCD_punto(x, y, color) escribiremos todas las funciones restantes.
x: un byte, es la coordenada "x" (horizontal), con valores válidos de 0 a 127 (izquierda a derecha).
y: un byte, es la coordenada "y" (vertical), con valores válidos de 0 a 63 (arriba a abajo)
color: un bit, "0" = apagado, "1" = encendido.
Los parametros que recibe GLCD_linea(x1, y1, x2, y2, color) son:
x1: un byte, es la coordenada "x" (horizontal) del primer extremo de la linea, con valores válidos de 0 a 127 (izquierda a
y1: un byte, es la coordenada "y" (vertical) del primer extremo de la linea, con valores válidos de 0 a 63 (arriba a abajo)
x2: un byte, es la coordenada "x" (horizontal) del segundo extremo de la linea, con valores válidos de 0 a 127 (izquierda
y2: un byte, es la coordenada "y" (vertical) del segundo extremo de la linea, con valores válidos de 0 a 63 (arriba a abaj
color: un bit, "0" = linea en blanco, "1" = linea en negro.
Los parametros que recibe GLCD_rectangulo(x1, y1, x2, y2, color) son:
x1: un byte, es la coordenada "x" (horizontal) de la esquina superior izquierda del rectángulo, con valores válidos de 0 a
derecha).
y1: un byte, es la coordenada "y" (vertical) de la esquina superior izquierda del rectángulo, con valores válidos de 0 a 6
x2: un byte, es la coordenada "x" (horizontal) de la esquina inferior derecha del rectángulo, con valores válidos de 0 a 1
derecha).
y2: un byte, es la coordenada "y" (vertical) de la esquina inferior derecha del rectángulo, con valores válidos de 0 a 63 (
color: un bit, "0" = rectángulo en blanco, "1" = rectángulo en negro.
(Ház clic sobre las imágenes para ampliarlas)
Los parametros que recibe GLCD_caja(x1, y1, x2, y2, color) son:
x1: un byte, es la coordenada "x" (horizontal) de la esquina superior izquierda del rectángulo, con valores válidos de 0 a
derecha).
y1: un byte, es la coordenada "y" (vertical) de la esquina superior izquierda del rectángulo, con valores válidos de 0 a 6
x2: un byte, es la coordenada "x" (horizontal) de la esquina inferior derecha del rectángulo, con valores válidos de 0 a 1
derecha).
y2: un byte, es la coordenada "y" (vertical) de la esquina inferior derecha del rectángulo, con valores válidos de 0 a 63 (
color: un bit, "0" = caja en blanco, "1" = caja en negro.
(Ház clic sobre las imágenes para ampliarlas)
x1: un byte, es la coordenada "x" (horizontal) del centro del circulo, con valores válidos de 0 a 127 (izquierda a derecha
y1: un byte, es la coordenada "y" (vertical) del centro del circulo, con valores válidos de 0 a 63 (arriba a abajo).
radio: un byte, es el radio de la circunferencia (en pixeles).
color: un bit, "0" = circulo en blanco, "1" = circulo en negro.
Para que la libreria pueda ser adaptada al proyecto que tienes en mente, hemos colocado los siguientes "#define" alc omienzo de
determinar que pin del PIC has conectado a cada pin del GLCD:
//Pines a usar
#define GLCD_CS1 PIN_E2
#define GLCD_CS2 PIN_E1
#define GLCD_DI PIN_C3
#define GLCD_RW PIN_C2
#define GLCD_E PIN_C1
#define GLCD_RESET PIN_E0
Si tu circuito emplea pines diferentes a los del ejemplo para manejar el GLCD, deberás cambiar los valores que sea necesario.
El display está "partido" en dos mitades de 64x64 pixeles. Esto implica que al momento de escribir en el debemos seleccionar en
mitades lo estamos haciendo. Para ello dispone de dos lineas de control (ver el pinout del GLCD en la sección correspondiente),
llamadas CS1 y CS2. Asignaremos los valores de 0 y 1 a "GLCD_lado_CS1" y "GLCD_lado_CS2", respectivamente.
Tambien hemos definido un BYTE que guardará el dato que leyamos desde el GLCD:
[editar]GLCD_enviaBYTE(lado, dato)
Esta función envia un byte a uno u otro lado del display. Como mencionamos antes, debemos seleccionar previamente, mediante
de CS1 o CS2, cual utilizaremos.
El parámetro (tipo int1) "lado" es el que define a que mitad del GLCD irá a parar el "dato".
El resto de la función no tiene ningún secreto. En ella, se realizan los siguientes pasos:
[editar]GLCD_leeBYTE(lado)
Igual que la anterior, recibe como parámetro la información que le indica en cual de las dos mitades está el dato a leer.
El equipo de uControl.
El que hemos usado en nuestras pruebas es el de la foto siguiente. Se trata de un ELWG12864-YYB-VN, con puntos negros sob
empresa Winstar.
IMPORTANTE: Si tu display no es exactamente este modelo, consulta su hoja de datos para asegurarte que funcion cumple ca
#include <STDLIB.H>
//Pines a usar
#define GLCD_RESET PIN_C4
#define GLCD_A0 PIN_C2
#define GLCD_RW PIN_C1
#define GLCD_E PIN_C0
#define GLCD_TRIS set_tris_b
#define GLCD_SAL output_b
#define GLCD_ENT input_b
//Definiciones varias
#define GLCD_WIDTH 128
#define ON 1
#define OFF 0
// Declaracion de funciones
void GLCD_Data(BYTE dato);
void GLCD_Command(BYTE dato);
BYTE GLCD_readBYTE();
void GLCD_clear(int1 color);
void GLCD_init(int1 modo);
void GLCD_pixel(int8 x, int8 y, int1 color);
void GLCD_line(int x1, int y1, int x2, int y2, int1 color);
void GLCD_rectangle(int x1, int y1, int x2, int y2, int1 color);
void GLCD_box(int x1, int y1, int x2, int y2, int1 color);
void GLCD_circle(int x1, int y1, int radio, int1 color);
void glcd_text35(int8 x, int8 y, char* textptr, int1 color);
//-----------------------------------------------------------------------
//Escribe un byte en la pantalla
//-----------------------------------------------------------------------
void GLCD_Data(BYTE dato)
{
output_low (GLCD_RW); // Modo escritura
output_high (GLCD_A0); // Modo escritura
GLCD_SAL (dato); // Coloco el dato a escribir
output_high (GLCD_E); // Aviso que hay un dato a escribir
delay_cycles( 2 ); // Espero
output_low (GLCD_E); // Termino la escritura del dato
delay_cycles( 2 ); // Espero
}
//-----------------------------------------------------------------------
//Envía un comando a GLCD
//-----------------------------------------------------------------------
void GLCD_Command(BYTE dato)
{
output_low (GLCD_RW); // Modo comando
output_low (GLCD_A0); // Modo comando
GLCD_SAL (dato); // Coloco el Comando a escribir
output_high (GLCD_E); // Aviso que estoy enviado un comando
delay_cycles( 2 ); // Espero
output_low (GLCD_E); // Termino el envío del comando
delay_cycles( 2 ); // Espero
}
//-----------------------------------------------------------------------
// Lee un byte de la pantalla
//-----------------------------------------------------------------------
BYTE GLCD_readBYTE()
{
BYTE dato;
GLCD_TRIS (0xFF); // Todo el Puerto como entrada de datos
output_high (GLCD_RW); // Modo lectura
output_high (GLCD_A0); // Modo lectura
output_high (GLCD_E); // Activo la lectura
delay_cycles( 2 ); // Espero
output_low (GLCD_E); // lectura tonta
delay_cycles( 2 ); // Espero
output_high (GLCD_E); // Activo la lectura
delay_cycles( 2 ); // Espero
dato = GLCD_ENT(); // Guardo en "dato" el valor devuelto
output_low (GLCD_E); // Termino la lectura
output_low (GLCD_RW); // Modo comando
output_low (GLCD_A0); // Modo comando
GLCD_SAL (0x00); // Limpio el Puerto de salida
GLCD_TRIS (0x00); // Todo el Puerto como salida de datos
return dato; // Retorno de la rutina con el dato
}
//-----------------------------------------------------------------------
// Limpia el GLCD (pinta toda la pantalla de un color)
//-----------------------------------------------------------------------
void GLCD_clear(int1 color)
{
int8 i, j;
for(i = 0; i < 7; ++i) // Recorre las 8 paginas (vertical)
{
for(j = 0; j < 127; ++j) // Recorre los 128 segmentos (horizontal)
{ GLCD_Data(0xFF * color); // Enciende/apaga pixeles
}
}
}
//-----------------------------------------------------------------------
//Esta funcion inicializa el LCD.
//-----------------------------------------------------------------------
void GLCD_init(int1 modo)
{
// Pone los pines de control en el estado correcto.
output_low(GLCD_RESET);
delay_ms(1);
output_high(GLCD_RESET);
GLCD_Command(0x40); // Linea Inicial = 0
GLCD_Command(0xA0); // ADC = normal
GLCD_Command(0xA3); // LCD-Bias = 1/7
GLCD_Command(0xC0); // output mode
GLCD_Command(0xAC); // static indicator = 0
GLCD_Command(0xA4); // Todos los pixeles apagados
GLCD_Command(0xA6); // Display no Invertido
GLCD_Command(0xE0); // read/modify/write
// Si modo = 1 inicializa encendido. Sino, apagado.
if(modo == 1)
{ GLCD_Command(0xAF); } // Enciendo el GLCD
else {
GLCD_Command(0xAE); } // Apago el GLCD
}
//-----------------------------------------------------------------------
// Dibuja un pixel
//-----------------------------------------------------------------------
void GLCD_pixel(int8 x, int8 y, int1 color)
{
BYTE dato;
int8 Posicion_Y;
int8 Posicion_X;
x = 127-x;
y = 63-y;
//Primero verificar que X e Y estan dentro de los límites
if ((x<=127)&(y<=63)){
Posicion_Y = y / 8; // Calculo La página a donde pertenece el bit
Posicion_Y = 0xB0 + Posicion_Y; // Genero el comando para ir a la página
GLCD_Command(Posicion_Y); // Ejecuto el comando
Posicion_X = x;
swap(Posicion_X);
Posicion_X &= 0x0F;
Posicion_X |= 0x10;
GLCD_Command(Posicion_X);
Posicion_X = x;
Posicion_X &= 0x0F;
GLCD_Command(Posicion_X);
dato = GLCD_ReadBYTE();
if(color == 1)
bit_set(dato, y%8); // Turn the pixel on
else // or
bit_clear(dato, y%8); // turn the pixel off
GLCD_Data(dato);
}
}
//-----------------------------------------------------------------------
// Dibuja una linea desde (x1,y1) a (x2,y2) de color (0 o 1)
//-----------------------------------------------------------------------
void GLCD_line(int x1, int y1, int x2, int y2, int1 color)
{
//Declaro variables-------------------
signed int x, y, incremento_x, incremento_y, distancia_x, distancia_y;
signed long P;
int i;
if(P < 0)
{ P += 2 * distancia_y;
x += incremento_x; }
else
{ P += 2*distancia_y - 2*distancia_x;
x += incremento_x;
y += incremento_y;}
}
}
//-----------------------------------------------------------------------
// Dibuja un rectángulo desde (x1,y1) a (x2,y2) de color (0 o 1)
//-----------------------------------------------------------------------
void GLCD_rectangle(int x1, int y1, int x2, int y2, int1 color)
{
GLCD_line(x1,y1,x2,y1,color);
GLCD_line(x1,y1,x1,y2,color);
GLCD_line(x1,y2,x2,y2,color);
GLCD_line(x2,y1,x2,y2,color);
}
//-----------------------------------------------------------------------
//-----------------------------------------------------------------------
// Dibuja un rectángulo PINTADO desde (x1,y1) a (x2,y2) de color (0 o 1)
//-----------------------------------------------------------------------
void GLCD_box(int x1, int y1, int x2, int y2, int1 color)
{
//Declaro variables-------------------
int i;
for(i=y1;i<=y2;i++) {
GLCD_line(x1,i,x2,i,color); }
}
//-----------------------------------------------------------------------
//-----------------------------------------------------------------------
// Dibuja un circulo con centro en (x1,y1), radio y color (0 o 1)
//-----------------------------------------------------------------------
void GLCD_circle(int x1, int y1, int radio, int1 color)
{
signed int d, x, y;
//Dibujo los cuatro pixeles que "caen" sobre los ejes cartesianos.
GLCD_pixel(x1, y1 + radio, color);
GLCD_pixel(x1, y1 - radio, color);
GLCD_pixel(x1 + radio, y1, color);
GLCD_pixel(x1 - radio, y1, color);
for(i=0; textptr[i] != '\0'; ++i, ++x) // Loop through the passed string
{
if((textptr[i] >= ' ') && (textptr[i] <= '~'))
memcpy(pixelData, TEXT35[textptr[i]-' '], 5);
else
memcpy(pixelData, TEXT35[0], 5); // Default to space
¡Gracias Ruben!
/////////////////////////////////////////////////////////////////////////
//// (C) Copyright 1996, 2004 Custom Computer Services ////
//// This source code may only be used by licensed users of the CCS ////
//// C compiler. This source code may only be distributed to other ////
//// licensed users of the CCS C compiler. No other use, ////
//// reproduction or distribution is permitted without written ////
//// permission. Derivative programs created using this software ////
//// in object code form are not restricted in any way. ////
/////////////////////////////////////////////////////////////////////////
"Este código fuente sólo puede ser utilizado por usuarios con licencia del compilador C CCS. Este código fuente sólo puede ser
usuarios con licencia del compilador C CCS. No se permite ningún otro uso, reproducción o distribución sin el consentimiento e
programas derivados, creados utilizando este software, pueden distribuirse como código objeto sin limitantes."
Mantener ordenadas aquellas librerías que compartimos entre distintosproyectos, puede ser una tarea a veces complicada, sobre
desconocemos métodos eficaces para realizar este trabajo. Este problema existe desde hace muchos años y los desarrolladores de
C, fueron incluyendo mecanismos eficientes para dar solución a este problema. Las siguientes líneas nos ayudarán a sacar prove
pero poderosas herramientas.
Tabla de contenidos
[mostrar]
El uso de las librerías es fundamental para el desarrollo de proyectos en “C”. Sin embargo, cuando tenemos varios proyectos que
mismas librerías, una gestión deficiente, puede llevarnos al caos.
En aquellas librerías que necesitamos reutilizar, y que por su naturaleza, tenemos que modificar para adaptarlas a nuestros proye
ejemplo de este tipo de librerías es la Flex_LCD.c desarrollada por CCS, que nos permite utilizar los muy comunes LCD’s de 2
Habitualmente, esta librería debe ser modificada para adaptarla a nuestras necesidades en cada proyecto. Esta situación se presen
proyectos requieren el uso de distintos microcontroladores o cuando necesitamos determinados módulos del microcontrolador, c
han sido asignados al LCD dentro de Flex_LCD.c. De aquí en adelante, utilizaremos la librería “Flex_LCD.c” como modelo par
artículo, pero todo lo expuesto es aplicable cualquier librería.
Aquí es donde surge el caos entre los distintos proyectos que tenemos entre manos o que hemos realizado. Analicemos las tres a
más frecuente:
Pero: ¿qué ocurre cuando debemos modificar y recompilar un proyecto hecho con anterioridad? Si los pines utilizados en el proy
actual coinciden, no tendremos problema alguno. Sin embargo, es frecuente que no coincidan los pines asignados al LCD del an
los del actual. Por lo que si compilamos un proyecto antiguo, es muy probable que no funcione correctamente.
La solución común al problema anterior, es tener anotado en algún lugar la asignación de pines para cada proyecto y modificar l
compilar cada uno. Como se pude ver, es un proceso tedioso que exige un alto grado de orden para mantener la funcionalidad de
[editar]El método de la copia
Una alternativa que puede solucionar el problema anterior, es tener una copia de la librería en el directorio de cada proyecto. Lue
copia, para ajustarla a la configuración según sea el caso. Esto permite que podamos compilar cada proyecto una y otra vez, sin n
modificar la librería, ya que cada proyecto tiene una copia adaptada según sus necesidades.
Es una solución también bastante habitual, pero no idónea; ¿qué ocurre si necesitamos modificar la librería porque tenemos una
misma? Tendremos que ir buscando por el laberinto de directorios de proyectos cada copia de la librería vieja y sustituirla por la
Se puede argumentar que hoy en día con la velocidad de proceso y las herramientas de búsqueda de las PC, este trabajo no será e
Pero aunque lográsemos encontrar y sustituir todas las copias en un corto espacio de tiempo, tendremos otro problema añadido, y
de la librería está “personalizada” para su proyecto. La situación anterior nos obliga a reconfigurar la nueva versión de la copia,
configuración de cada proyecto, trabajo que hicimos la primera vez que copiamos la librería hacia el directorio del proyecto.
Las directivas del pre-procesador son un conjunto de instrucciones que se utilizan para indicarle al compilador, que debe hacer, a
situaciones. Aunque generalmente muchos programadores desconocen su utilidad con profundidad, estas directivas son una herr
poderosa para crear variables, reservar memoria, definir constantes, utilizar macros e incluso indicarle al compilador que seccion
compilar y enlazar. En nuestro caso, utilizaremos las directivas del pre-procesador #ifndef <identifier> … #endif.
Cuando el pre-procesador se topa con la directiva #ifndef, comprueba si ya existe el identificador <identifier>, si éste no existies
con ese nombre, lo agrega a su lista de identificadores y procesa el código ubicado entre #ifndef y #endif, en caso que el identific
exista, se ignora todo el código ubicado en el cuerpo de la llamada a la directiva.
La técnica descrita anteriormente es precisamente la que vamos a utilizar para gestionar de manera eficiente, el uso de nuestras l
sección de Flex_LCD, donde se asignan los pines al microcontrolador, nos topamos con el siguiente código:
#ifndef _FLEX_LCD
#define _FLEX_LCD
#define LCD_DB4 PIN_B4
#define LCD_DB5 PIN_B5
#define LCD_DB6 PIN_B6
#define LCD_DB7 PIN_B7
#define LCD_RS PIN_C0
#define LCD_RW PIN_C1
#define LCD_E PIN_C2
#endif
Si no definimos nada en el programa principal o en su fichero de cabecera, el pre-procesador asignará a la LCD los pines según e
librería Flex_LCD. Si queremos modificar la asignación de pines para nuestro proyecto, escribiremos en el fichero principal de
en su fichero de cabecera, el siguiente fragmento de código:
#define _FLEX_LCD
#define LCD_DB4 PIN_C4
#define LCD_DB5 PIN_C5
#define LCD_DB6 PIN_C6
#define LCD_DB7 PIN_C7
#define LCD_RS PIN_A0
#define LCD_RW PIN_A1
#define LCD_E PIN_A2
#include Flex_LCD.c
Esto hace que se asignen los pines del microcontrolador a la LCD tal y como se especifica en nuestro programa principal y que l
librería sea ignorada. Como puede verse, la librería ha sufrido un pequeño cambio que nos ayudará a mantener gestionado su uso
vida a partir de este momento. Es muy importante que esta asignación se haga antes de incluir la librería (#include Flex_LCD.c),
hacerlo así, el pre-procesador asignará los pines según la definición que se hace dentro de la librería y se producirá un conflicto c
realizada en el programa principal.
Con este método, solo tendremos una librería para todos nuestros proyectos y la personalización se realizará dentro de cada proy
tengamos que hacer copias o modificar el fichero original. Además, la librería estará perfectamente localizable dentro de su direc
obtuviésemos una nueva versión, bastará con actualizar y modificar una sola copia.
Otra razón para utilizar esta forma de proceder, es la posibilidad de reconocer la dependencia entre los distintos archivos de nues
entre distintas librerías. Por ejemplo, si creamos una librería que utilice el display como salida, podremos escribir en el código de
#ifndef _FLEX_LCD
#error Es necesario incluir la librería Flex_LCD
#endif
De esta forma enviamos un mensaje de error para avisar que es preciso incluir una o varias librerías.
#DEFINE
#ELIF
#ELSE
#ENDIF
#ERROR
#IF
#IFDEF
#IFNDEF
#INCLUDE
#LIST
#NOLIST
#PRAGMA
#UNDEF
• Directivas asociadas a las bibliotecas precompiladas, que
proporcionan al compilador información relacionada con estas
bibliotecas:
#USE DELAY
#USE FAST_IO
#USE FIXED_IO
#USE I2C
#USE RS232
#USE STANDARD_IO
• Directivas relacionadas con la especificación del dispositivo,
por un lado, para definir los mapas de memoria y el juego de
instrucciones, y por otro, incluir información necesaria para la
programación del dispositivo en los ficheros de salida de la
compilación:
#DEVICE
#ID
#FUSES
#TYPE
• Directivas de cualificación de funciones, para identificar
características especiales de una función:
#INLINE
#INT_DEFAULT
#INT_GLOBAL
#INT_xxxxx
#SEPARATE
• Directivas de control del compilador, para definir opciones
referidas a la compilación del código del programa:
#CASE
#OPT
#ORG
#PRIORITY
• Directivas de control de la memoria del microcontrolador,
para gestionar y reservar el uso de determinadas zonas de
memoria para variables:
#ASM
#BIT
#BYTE
#ENDASM
#LOCATE
#RESERVE
#ROM
#ZERO_RAM
• Identificadores predefinidos. Todas las directivas citadas
hasta ahora, son comandos destinados a ser interpretados por el
compilador, no por el microcontrolador. Dentro del término
genérico de directiva se incluyen, además de estos comandos,
unas variables que contienen información sobre el proceso de
compilación. Estas variables son lo que se denominan
identificadores predefinidos del compilador:
__DATE__
__DEVICE__
__PCB__
__PCH__
__PCM__
Funciones precompiladas.
Utilidades adicionales
El entorno PCW incluye, además del IDE y del compilador, una serie
de utilidades adicionales con las que se amplían las posibilidades de
éste, y que se encuentran en los menús View y Tools de la barra de
menús, veamos algunas de ellas: