Beruflich Dokumente
Kultur Dokumente
Introducción
Esta guia o tutorial esta dedicada al lenguaje de programación C, concretamente al del compilador CCS. A lo largo de este
artículo iremos viendo los rudimentos del lenguaje y el repertorio de instrucciones incluidas en él. Este puede ser un buen
punto de partida para aquellos programadores que están empleando algún dialécto BASIC, como el PIC BASIC incluido
en el PIC SIMULATOR IDE, PIC BASIC PRO (PBP) o PROTON
CCS - Comentarios
Los comentarios son útiles para informar al que lee nuestro código (o a nosotros mismos)el significado o funcionamiento
de cada parte del programa. Todos los comentarios son ignorados por el compilador, por lo que no debes preocuparte por
llenar la memoria del PIC.
Un comentario puede ser colocado en cualquier parte del programa, excepto en medio de una palabra reservada, del
nombre de una función o del nombre de una variable.
Los comentarios puede ocupar mas de una linea de largo. Pueden utilizarse para desabilitar momentanemente un trozo de
código.
Hay dos formas de introducir un comentario. La primera es la misma que en cualquier otro compilador de C:
/* Esto es un comentario */
Es decir, todo lo que haya escrito entre /* y */ sera tomado por el compilador como un comentario.
// Esto es un comentario
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.
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.
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
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.
Asignación de valores
Asignar un valor a una variable es una tarea bien simple. Basta con hacer lo siguiente:
nombre_de_variable = valor;
donde nombre_de_variable es el nombre de la variable que contendra el valor. Al igual que todas las instrucciones de
CCS, debe terminar con un ; (punto y coma).
Por ejemplo, supongamos que queremos asignar el valor "100" a la variable "count". Lo hacemos de la siguiente manera:
count = 100;
Podemos asignar un valor a una variable en el momento en que la declaramos. lo siguientes son algunos ejemplos de esto:
int a = 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;
Varibles Locales y Globales
Si una variable se declara dento de una funcion, será "visible" solo dentro de ésta:
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().
Conversiones entre tipos
CCS nos permite mezclar diferentes tipos de variables dentro de una misma expresión. Y existen un conjunto de reglas que
nos permiten saber que de que tipo será el resultado de la misma.
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.
CCS calcula 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);
Las directivas más comunes son #define e #include, pero deberías dar un vistazo a todas.
#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:
La variable predefinida _RETURN_ puede utilzarse para transferir un valor desde el código ASM a 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").
#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:
donde nombre es un nombre de variable CCS válido, x es una constante o una variable CCS válida e y es una constante de
0 a 7.
int resultado;
#BIT resultado_primer_bit = resultado.0
.
.
.
if (resultado_primer_bit)
#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
donde nombre es un nombre de variable CCS válido, y x es una constante o una variable CCS válida.
#BYTE STATUS = 3
#BYTE PORTB = 6
#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 TRUE, el precompilador la reemplazará por 1
#DEFINE pi 3.14159265359
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.
#DEFINE es una potente herramienta para la creación de macroinstrucciones, ya que soporta el uso de variables. Veamos
algunos ejemplos de esto:
Cuando el preprocesador se encuentra con el código anterior, hace lo mismo que si hubiesemos escrito lo siguiente:
Como puedes ver, #DEFINE puede hacer mucho por tus programas.
#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 microcontrolador seleccionado.
#DEVICE PIC16F877 *=16 ADC=10 //PIC 1616F877, punteros de 16 bits y 10 bits en el ADC.
WRITE_EEPROM=ASYNC :
HIGH_INTS=TRUE : Define la prioridad de las interrupciones en los PIC18.
#FUSE
Permite modificar el valor de los fuses del microcontrolador que estamos empleando. Los valores posibles dependen de
cada microcontrolador 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.
#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.
Existe la posibilidad de utilizar #INCLUDE "archivo" en lugar de #INCLUDE <archivo>. La diferencia es que si
usamos "", el archivo se buscará primero en el directorio actual. Si empleamos <>, el archivo será buscado primero en la
ruta por defecto para los archivos .h.
#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() {
...
}
CCS - Operadores
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 de operadores y operandos. En la mayoría de los casos, los operadores de CCS siguen las
mismas reglas que en álgebra, y se llaman de la misma manera.
Operadores aritméticos
CCS posee cinco operadores aritméticos:
+ (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.
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
Operadores Relacionales
Los operadores relacionales comparan dos valores, y devuelven un valor lógico basado en el resultado de la comparación.
Los operadores relacionales disponibles son los siguientes:
el resultado de la comparación, sera siempre 0 o 1. 0 significa que el resultado de la comparación ha sido falso, y 1 que ha
sido verdadero.
Operadores Lógicos
Los operadores lógicos disponibles permiten realizar las operaciones AND, OR y NOT:
a = b && ( q || n )
Operadores de bits
Existen seis operadores pensados para trabajar directamente sobre los bits. Solamente pueden usarse con variables tipo int
y char. Son los siguientes:
& (AND)
| (OR)
^ (XOR)
~ (complemento)
<< (desplazamiento a la izquierda)
>> (desplazamiento a la derecha)
Estas operaciones se llevan a cabo bit por bit. Veamos un ejemplo:
a&b=8
a | b = 125
a ^ b = 117
~ a = 135
El porqué de estos resultados puede comprenderse mejor si se pasan los valores de a y b a binario:
a = 11111000
b = 00001101
luego
Los operadores de desplazamiento "corren" el contenido de la variable a la derecha o a la izquierda, rellenando con ceros.
Veamos algunos ejemplos:
Si a era igual a 120 ( 01111000 en binario) pasará a valer 192 (11000000 en binario).
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
a++
o asi:
a--
el resultado sera el mismo, pero es mas corto de escribir, y mas fácil de utilizar en expresiones complejas.
()
signo +, signo -, ++, --, !, (<tipo>)
*, /, %
+, -
<, <=, >, >=
==, !=
&&, ||
=, +=, -=, *=, /=, %=
CCS - Punteros
Una de las caracteristicas mas interesantes de las diferentes versiones de C son los punteros. Por supuesto, CCS permite el
manejo de punteros, con 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@gmail.com.
¿Qué es un puntero?
Un puntero es una variable cuya finalidad es almacenar números ENTEROS POSITIVOS. Estos números no son números
al azar, son direcciones de la memoria que posee el hardware del microcontrolador (memoria de programa o RAM).
Cuando se crea una variable en CCS (llamado registro en ensamblador), el compilador reserva un espacio de memoria cuyo
tamaño varia de acuerdo al tipo de dato.
Conociendo la dirección del registro o variable y pudiéndolo manejar nos da un poderosa herramienta para
agilizar/simplificar nuestros programas.
¿Como podemos acceder a la dirección de una variable?
En CCS se hace a través del operador &. Veamos un ejemplo:
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 5. todo se realiza usando memoria RAM ó el registro de propósito general GPR.
#include <18F4550.h>
#use delay(clock=4000000)
void main(){
int k,l,m;
int t,u,v;
t=0xfa; u=0xfb; v=0xfc;
k= &t; l= &u; m= &v;
delay_cycles(1);
}
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:
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..255). u es un dato "entero largo", ocupa dos bytes (0..65535) v es un dato "coma flotante", con parte fraccionaria en el
sistema decimal y toma 4 bytes de memoria (32 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
separación:
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 artículo. Vamos a 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:
Podran ovservar que se usa el puntero sin el *. Esto significa que se guardará allí una dirección y el compilador lo
interpreta de esa manera.
no quiere decir que el tipo de datos que contendrá el puntero sea de ese tipo de datos, el puntero siempre soportará números
enteros positivos, en 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);
}
Entre i, p hay 1 byte -> i ocupa 1 byte.
Entre p, j hay 2 bytes -> puntero p ocupa 2 bytes.
Entre j, q hay 1 byte -> j ocupa 1 byte
Entre q, k hay 2 bytes -> puntero q ocupa 2 bytes.
#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 el compilador el 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 1 byte de dirección 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);
}
Aquí se lee que está correcto el resultado.
Nota: los punteros tiene un máximo de 2 bytes para almacenar direcciones y el CCS sigue la misma normativa. Hay una
directiva llamada #device xxxxxxx
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 de 14, 16 bits y con CCS4 (modo por defecto) el size es de 2 bytes.
Probando punteros, segunda parte
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 tamaño del apuntado son 2 cosas distintas, vamos hacer un ejemplo donde se verá claramente. Para ello vamos a usar
una directiva llamada #locate, sobre la que la ayuda 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 así como si en ensamblador pusieramos:
Esto nos servirá porque sería como manipular el contenido de un puntero pero en tiempo de diseño
#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 hoja de datos). Para que 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 memoria de datos y las direcciones se deberían leer como puros 0x0 a pesar que compiló bien, (similarmente en
programas de computadoras pueden ocurrir los lazos infinitos popularmente llamado „se colgó la máquina‟)
Punteros en funciones
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 define así:
Una función es una relación entre dos variables numéricas, habitualmente las denominamos x e y; a una de ellas la
llamamos variable dependiente 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 suele ser la x. Pero 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 variable dependiente, no 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 dato de salida. Y 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 pasemos el dato 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 regresa un dato con el 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 la variable es local, 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á a la función es la 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
Como sabrán los arrays son arreglos de datos que van en direcciones consecutivas, es decir, uno detrás del otro. Un
ejemplo de ello:
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;
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
sumando direcciones que 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
direcciones de números 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 esto? Es que p quedará 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 y la solución es la 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!
Arreglando el programa con *(p+t)
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 hizo en el código del inicio del artículo?
O sea que ¿ c = cadena[t]; es igual a c = *(p + t) cuando p = &cadena[0]; ?
Pues si, acabamos de ver un array al desnudo, como funciona en realidad, no es mas que un puntero escondido a nuestra
vista. Solo que para hacer 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) es totalmente 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 puede modificar).
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
Programando en CCS.
Las funciones son los bloques básicos con los que construimos un programa en CCS. Además de la funcion main() que
veremos enseguida, un programa CCS tendrá seguramente varias funciones más, conteniendo cada una un bloque de
instrucciones que realizan una tarea determinada.
Funciones
Las funciones tienen la siguiente forma:
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
utilizarlas.
Prototipos
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 termina con ; (punto y coma).
El siguiente ejemplo declara la funcion ejemplo() que devuelve como resultado un valor del tipo long:
long ejemplo();
Parámetros
Ademas de determinar el tipo de resultado que devolverá la función, en el prototipo podemos especificar que parametros
recibirá, y de que tipo serán. La forma de hacerlo es la siguiente:
La diferencia con el caso anterior es que se han incluido dentro de los () una serie de nombres de variables (var1, var2, ...,
varN), cada una asociado a 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, y nos devuelva el 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);
Vemoslo con el ejemplo de la función suma vista mas arriba. La función podría ser como sigue:
Void
void significa que la función no devolverá ningún parametro. Supongamos que la función ejemplo() no debe regresar
ningún valor luego de ser 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);
main() {
instruccion;
instruccion;
.
.
instruccion; }
donde instruccion; puede ser cualquier instrucción válida del CCS o una llamada a otra función.
En CCS no disponemos de instrucciones específicas para el manejo de pantallas LCD. Sin embargo, nada impide que
escribamos funciones que sean 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 LCD esten construidos en base al controlador Hitachi HD44780.
LCD.C
Para ponernos las cosas más faciles, dentro de la carpeta "drivers" de CCS se encuentra un archivo llamado LCD.C, que si
lo incluimos en nuestro proyecto, nos proveerá de las funciones necesarias. Sin embargo, LCD.C tiene algunas
limitaciones: tal como está, solo funciona si conectamos 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 para que se pueda 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 provisto por CCS.
///////////////////////////////////////////////////////////////////////////
//// 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);
}
Funciones en LCD.C
lcd_init()
Esta funcion es la encargada de enviar los comando de inicializacion necesarios al LCD. Es obligatorio ejecutar esta
función antes de utilizar el 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.
lcd_putc()
Esta seguramente será la función que mas emplearemos. Es la que se encarga de escribir nuestro mensaje en la pantalla. No
devuelve valores, pero si (obviamente) los recibe. La forma de uso es muy simple. Básta con llamarla, pansandole como
parámetro una variable o constante tipo char, y la 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 que pueden ser enviados en el texto a mostrar:
Esto quiere decir que si modificamos nuestro código para que quede así:
Lcd_putc ("\f");
lcd_gotoxy(x,y)
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 tipo byte. El primero de 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 siguiente código 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:
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 devuelve un char con el contenido. Su uso no podría ser más sencillo:
char a;
a = lcd_getc(5,2);
Notar que no estámos usando el pin RW del display, que estará permanentemente conectado a GND. La estructura deberia
quedar asi:
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 linea
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. El listado siguente corresponde al archivo LCD.C con todas las modificaciones mencionadas, más algunas
modificaciones en los #INCLUDE del principio, que no tienen sentido mantener ya que al personalizar el archivo nunca se
van a dar algunas de las condiciones contempladas alli. También hemos quitado el código de la función lcd_getc( x, y) ya
que al estar RW conectado de forma permanente a GND, será imposible leer caracteres del display.
///////////////////////////////////////////////////////////////////////////
// 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;
}
}
Libreria de gráficos para GLCD K0108 en CCS
Display GLCD
El compilador CCS proporciona una libreria capaz de dibujar primitivas sobre varios modelos de displays LCD gráficos o
GLCD (por Graphic Liquid Cristal Display). Hay versiones de esta libreria para pantallas con diferentes controladores
embebidos, como el Samsung KS0108 o el Toshiba T6963.
Pero a pesar de que pueden distribuirse libremente los trabajos que hagamos con ellas, no pueden compartirse los
programas que las contengan a 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 GLCD, estariamos 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.
La librería GLCD_K0108
En las siguientes secciones iremos explicando cada una de sus partes.
IMPORTANTE: El trazado de lineas se basa en el Algoritmo de Bresenham, y las circunferencias se han resuelto
mediante el "algoritmo del punto medio", que divide la circunferencia en 8 partes simétricas, evitando utilizar funciones
como seno, coseno o potencias, que volverian muy lenta la tarea del trazado.
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 de negro. Si recibe un "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 mitades del GLCD, 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, mediante
GLCD_limpiar(0);, se pinta completamente de blanco. Podemos usar esta función para limpiar la pantalla.
GLCD_inicializa(modo)
Se encarga de inicializar el GLCD, y el parametro "modo" determina si estará encendido (si recibe un "1") o apagado (si
recibe un "0").
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
derecha).
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 a
derecha).
y2: un byte, es la coordenada "y" (vertical) del segundo extremo de la linea, con valores válidos de 0 a 63 (arriba a abajo).
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 127
(izquierda 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 63
(arriba a abajo).
x2: un byte, es la coordenada "x" (horizontal) de la esquina inferior derecha del rectángulo, con valores válidos de 0 a 127
(izquierda a 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 (arriba
a abajo).
color: un bit, "0" = rectángulo en blanco, "1" = rectángulo en negro.
Las "cajas" son rectángulos pintados en su interior con el mismo color que el borde exterior. También se dibujan
(internamente) mediante llamadas a la función GLCD_linea.
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 127
(izquierda 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 63
(arriba a abajo).
x2: un byte, es la coordenada "x" (horizontal) de la esquina inferior derecha del rectángulo, con valores válidos de 0 a 127
(izquierda a 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 (arriba
a abajo).
color: un bit, "0" = caja en blanco, "1" = caja en negro.
Esta es la función que dibuja un circulo. El interior del circulo permanece del color del fono. Estrictamente hablando, se
dibuja solo la circunferencia.
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.
//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 cual de las dos 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:
GLCD_enviaBYTE(lado, dato)
Esta función envia un byte a uno u otro lado del display. Como mencionamos antes, debemos seleccionar previamente,
mediante la activación 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:
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 que hemos usado en nuestras pruebas es el de la foto siguiente. Se trata de un ELWG12864-YYB-VN, con puntos
negros sobre fondo verde, de la empresa Winstar.
IMPORTANTE: Si tu display no es exactamente este modelo, consulta su hoja de datos para asegurarte que funcion
cumple cada uno de sus pines.
Restricciones legales de CCS
Como deciamos en la introducción, dentro de las librerias incluidas en CCS se ha incluido una especie de licencia.
Concretamente, en el código fuente de dichas librerias puede leerse algo como lo siguiente:
/////////////////////////////////////////////////////////////////////////
//// (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 distribuido a otros usuarios con licencia del compilador C CCS. No se permite ningún otro uso, reproducción o
distribución sin el consentimiento escrito. Los programas derivados, creados utilizando este software, pueden distribuirse
como código objeto sin limitantes."
El uso de las librerías es fundamental para el desarrollo de proyectos en “C”. Sin embargo, cuando tenemos varios
proyectos que comparten las mismas librerías, una gestión deficiente, puede llevarnos al caos.
Habitualmente, esta librería debe ser modificada para adaptarla a nuestras necesidades en cada proyecto. Esta situación se
presenta cuando nuestros proyectos requieren el uso de distintos microcontroladores o cuando necesitamos determinados
módulos del microcontrolador, cuyos pines de E/S, han sido asignados al LCD dentro de Flex_LCD.c. De aquí en adelante,
utilizaremos la librería “Flex_LCD.c” como modelo para el resto del artículo, pero todo lo expuesto es aplicable cualquier
librería.
¿Como se modifican estas librerías para su uso?
Aquí es donde surge el caos entre los distintos proyectos que tenemos entre manos o que hemos realizado. Analicemos las
tres alternativas, de uso más frecuente:
Tenemos una única librería ubicada en el directorio de librerías (library), y cuando nos hace falta, la modificamos. Esta
suele ser una práctica muy habitual. Cada vez que empezamos un nuevo proyecto modificamos la librería y la adaptamos a
la necesidad del momento.
Pero: ¿qué ocurre cuando debemos modificar y recompilar un proyecto hecho con anterioridad? Si los pines utilizados en el
proyecto anterior y el actual coinciden, no tendremos problema alguno. Sin embargo, es frecuente que no coincidan los
pines asignados al LCD del antiguo proyecto con 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 la librería antes de compilar cada uno. Como se pude ver, es un proceso tedioso que exige un alto grado de orden
para mantener la funcionalidad de nuestros proyectos.
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. Luego modificamos la copia, para ajustarla a la configuración según sea el caso. Esto permite que podamos
compilar cada proyecto una y otra vez, sin necesidad de 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 nueva versión de la misma? Tendremos que ir buscando por el laberinto de directorios de proyectos cada copia de la
librería vieja y sustituirla por la nueva.
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á en extremo tedioso. Pero aunque lográsemos encontrar y sustituir todas las copias en un corto espacio de tiempo,
tendremos otro problema añadido, y es que cada copia de la librería está “personalizada” para su proyecto. La situación
anterior nos obliga a reconfigurar la nueva versión de la copia, de acuerdo a la configuración de cada proyecto, trabajo que
hicimos la primera vez que copiamos la librería hacia el directorio del proyecto.
Esta es la forma correcta y eficaz de hacerlo. Este método es el que adoptaremos y nos permitirá manejar las librerías sin
sufrir dolores de cabeza. Consiste en definir la asignación de pines, en algún lugar fuera de la librería, bien en fichero
aparte, o bien en el programa principal del proyecto. ¿Cómo podemos modificar la asignación de pines fuera de la librería?
La forma de hacerlo es utilizando las directivas del pre-procesador.
Las directivas del pre-procesador son un conjunto de instrucciones que se utilizan para indicarle al compilador, que debe
hacer, ante determinadas situaciones. Aunque generalmente muchos programadores desconocen su utilidad con
profundidad, estas directivas son una herramienta muy poderosa para crear variables, reservar memoria, definir constantes,
utilizar macros e incluso indicarle al compilador que secciones de código debe 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
existiese, entonces crea uno con ese nombre, lo agrega a su lista de identificadores y procesa el código ubicado entre
#ifndef y #endif, en caso que el identificador [[<identifier>]] 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 librerías. Al revisar la sección de Flex_LCD, donde se asignan los pines al microcontrolador, nos topamos con el
siguiente código:
Ahora simplemente metemos esta sección de código en el cuerpo de una llamada a #ifndef con nombre de identificador
_FLEX_LCD, el código resultante quedará de la siguiente forma:
#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 el código de la librería Flex_LCD. Si queremos modificar la asignación de pines para nuestro proyecto, escribiremos
en el fichero principal de nuestro proyecto, o 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 la definición de la librería sea ignorada. Como puede verse, la librería ha sufrido un pequeño cambio que nos ayudará a
mantener gestionado su uso y nos facilitará la 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), ya que de no 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 con la definición 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
proyecto; sin que por ello tengamos que hacer copias o modificar el fichero original. Además, la librería estará
perfectamente localizable dentro de su directorio, por lo que si 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
nuestros proyectos o entre distintas librerías. Por ejemplo, si creamos una librería que utilice el display como salida,
podremos escribir en el código de nuestra librería:
#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.