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 pueden ocupar más de una línea de largo. Pueden utilizarse para deshabilitar momentáneamente 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 */ será tomado por el compilador como un comentario. La segunda manera es la siguiente: // Esto es un comentario En este caso, el comentario comienza en // y se extiende hasta el final de la línea.
CCS - Variables 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: Tipo
Ancho (Bits)
Rango
short
1
0o1
short int
1
0o1
int
8
0 a 255
char
8
0 a 255
unsigned
8
0 a 255
unsigned int
8
0 a 255
signed
8
-128 a 127
signed int
8
-128 a 127
long
16
0 a 65536
long int
16
0 a 65536
signed long
16
-32768 a 32767
float
32
3.4E-38 a 3.4E+38
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.
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 demás instrucciones CCS que veremos a lo largo de este tutorial, debe terminar con ;. La forma en que se declara una variable es la siguiente: tipo nombre_de_la_variable; Donde tipo es alguno de los enumerados en la tabla anterior. Veamos un ejemplo: int temperatura; Esa línea permite a nuestro programa emplear la variable temperatura, que será 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 contendrá 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; donde 100 es una constante. Podemos asignar un valor a una variable en el momento en que la declaramos. lo siguientes son algunos ejemplos de esto: int a = 0; Hace que la variable a sea del tipo entero, y le asigna el valor 0. signed long a = 125, b, c = -10; a,b y c son declaradas como long. a toma el valor de "125" y c "-10". Si la variable es de tipo char, la constante que se le asigna debe estar entre tildes, como en el siguiente ejemplo: char nombre = 'juan perez'; 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;
Variables Locales y Globales Si una variable se declara dentro de una función, será "visible" solo dentro de ésta:
funcion1 () { char letra; . . . .} En el ejemplo anterior, la variable tipo char llamada letra solo podrá utilizarse dentro de la función funcion1 (). Si intentamos utilizarla fuera de ella, el compilador nos dará un error. Si declaramos una variable fuera de cualquier función, 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á automáticamente 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: int a = 250, b = 10; long c; c = a * b; Tal como explicamos, c no contendrá el valor 2500 como podría parecer a simple vista, por que el tipo de c no se modifica. 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. Si hubiésemos hecho: int a = 250, b = 10; long c; c = (long) (a * b);
el valor almacenado en c hubiese sido efectivamente 2500.
CCS - Directivas para el compilador Llamadas en inglés "preprocessor directives", son comandos que interpreta el primer paso de la compilación que lleva a cabo CCS. 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: int find_parity (int data)
{
int count; #ASM movlw 0x8 movwf count movlw 0 loop: xorwf data,w rrf
data,f
decfsz count,f goto
loop
movlw 1 awdwf count,f movwf _return_ #ENDASM }
La variable predefinida _RETURN_ puede utilizarse 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 memoria automáticos para las variables que no pueden ser accedidas desde el banco actual. El código 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
pueden ser empleadas de la misma manera que cualquier otra variable tipo short. El formato de #BIT es el siguiente: #BIT nombre = x.y 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. Estos son algunos ejemplos de uso: #BIT T0IF = 0xb.2 . . . T0IF = 0; // Limpia el flag de interrupción del Timer 0 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 pueden 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. Estos son algunos ejemplos de uso: #BYTE STATUS = 3 #BYTE PORTB = 6
#DEFINE La instrucción #define tiene la siguiente forma: #DEFINE value es la etiqueta que usaremos en nuestro programa. Y value es el valor que estamos asignando a esta etiqueta. Las instrucciones #DEFINE no generan código 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 fáciles de mantener. Veamos algunos ejemplos de #DEFINE #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 #DEFINE MENOR_DE_EDAD (EDAD < 18) . . . . . if MENOR_DE_EDAD printf(“JOVEN”); 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 difíciles 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: #DEFINE var(x,v) unsigned int x=v; var(a,1) var(b,2) var(c,3) Cuando el preprocesador se encuentra con el código anterior, hace lo mismo que si hubiésemos escrito lo siguiente: unsigned int a=1; unsigned int b=2; unsigned int c=3; 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. Veamos algunos ejemplos: #DEVICE PIC16C74
//PIC 16C74, punteros de 8 bits.
#DEVICE PIC16C67 *=16 //PIC 16C67, punteros de 16 bits. Hay más opciones que podemos agregar en las líneas #DEVICE:
ADC=x : Determina el número de [bit]]s que devuelve la función read_adc().
#DEVICE PIC16F877 *=16 ADC=10 //PIC 1616F877, punteros de 16 bits y 10 bits en el ADC.
ICD=TRUE : Genera código compatible con el ICD de [www.microchip.com Microchips]].
#DEVICE PIC16F877 ICD=TRUE//PIC 1616F877, punteros de 8 bits y código para ICD.
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 #INCLUDE 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. Algunos valores comunes son
Tipo de oscilador: LP, XT, HS, RC
Wach Dog Timer: WDT, NOWDT
Protección de código: PROTECT, NOPROTECT
Power Up Timer: PUT, NOPUT
Brown Out Reset: BROWNOUT, NOBROWNOUT #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. La forma de utilizar esta instrucción es la siguiente: #INCLUDE Esto hará que el contenido de se compile junto con nuestro programa. Por ejemplo: #INCLUDE 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 . 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_xxx indica 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: 1. INT_AD Conversión A/D finalizada. 2. I NT_ADOF Conversión A/D timeout. 3. INT_BUSCOL Colisión en bus. 4. INT_BUTTON Pushbutton. 5. INT_CCP1 Unidad CCP1.
6. INT_CCP2 Unidad CCP2. 7. INT_COMP Comparador. 8. INT_EEPROM Escritura finalizada. 9. INT_EXT Interrupción externa. 10. INT_EXT1 Interrupción externa #1. 11. INT_EXT2 Interrupción externa #2. 12. INT_I2C Interrupción por I2C. 13. INT_LCD Actividad en el LCD. 14. INT_LOWVOLT Bajo voltaje detectado. 15. INT_PSP Ingreso de datos en el Parallel Slave Port. 16. INT_RB Cambios en el port B (B4-B7). 17. INT_RC Cambios en el port C (C4-C7). 18. INT_RDA Datos disponibles en RS-232. 19. INT_RTCC Desbordamiento del Timer 0 (RTCC). 20. INT_SSP Actividad en SPI o I2C. 21. INT_TBE Buffer de transmisión RS-232 vacío. 22. INT_TIMER0 Desbordamiento del Timer 0 (RTCC). 23. INT_TIMER1 Desbordamiento del Timer 1. 24. INT_TIMER2 Desbordamiento del Timer 2. 25. INT_TIMER3 Desbordamiento del Timer 3. 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. Quizás C sea uno de los lenguajes que más operadores tiene. Una expresión es una combinación 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 ejemplos 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. 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 rápidamente. 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:
> mayor que >= mayor que o igual a < menor que > 2 //"corre" el contenido de a dos lugares a la derecha Si a era igual a 120 ( 01111000 en binario) pasará a valer 30 (00011110 en binario). 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 Otros operadores
Quedan por ver aun dos operadores más:
++ Operador incremento -- Operador decremento
Estos operadores permiten sumar (o restar) uno al valor de una variable. Lo que generalmente haríamos así: a=a+1
0 así: a=a-1 lo podemos hacer así: a++ o así: a-el resultado sera el mismo, pero es mas corto de escribir, y mas fácil de utilizar en expresiones complejas. Precedencia de los operadores Al igual que ocurre en álgebra, en CCS los operadores se evalúan en un orden determinado. La siguiente lista muestra este orden, ordenado de mayor a menor:
() signo +, signo -, ++, --, !, () *, /, % +, = ==, != &&, || =, +=, -=, *=, /=, %=
CCS - Punteros Una de las características más interesantes de las diferentes versiones de C son los punteros. Por supuesto, CCS permite el manejo de punteros, con lo que nuestros programas 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 electrónico es
[email protected]. ¿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). ¿Para que pueden servir los punteros? Esta es la pregunta que puede alborotar a mas de un programador de C. Sirve para muchísimas cosas:
Acceso a la memoria RAM del PIC.
Ahorrar memoria RAM.
Modificar más de una variable dentro de una función (y por consiguiente devolver mas de un valor)
En arreglos y cadenas strings (arrays, matrices) juega un papel importantísimo.
Permite crear tablas con montones de datos (en los PIC que soporten acceso a la memoria de programa).
En un ordenador se amplía el abanico de opciones.
Más abajo veremos detalladamente como hacer todo esto.
¿Como funcionan los punteros? Para entender el uso de estas variables especiales hay que comprender bien un concepto: 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. Como todo en el mundo electrónico/digital, está basado en 2 cosas:
El registro: es la casilla donde se almacena el dato.
La dirección del registro: es la posición en la memoria donde está alojado el registro.
así pues tenemos 2 elementos diferentes pero que se relacionan. 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 #use delay (clock=4000000) void main(){ int t,k; t=5; k= &t; delay_cycles (1); } al simular en el MPLAB tenemos:
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. Vamos a cambiar ligeramente el código. Usemos 3 variables tipo entero (int): #include #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 #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..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)
en t tenemos una dirección que ocupa un byte [0xA]
en u tenemos una dirección que ocupa 2 byte [0xB - 0xC]
en v tenemos una dirección que ocupa 4 bytes [0xD - 0x10]
Probando punteros, primera parte Siempre que se declare una variable puntero, al momento de usarlo se debe especificar la dirección de apuntamiento de la variable normal, porque entonces no se puede guardar un dato sino sabemos donde lo vamos a guardar. (Es obvio pero es cierto) Esto quiere decir que se le debe pasar el número por valor de la dirección de la variable normal. Recordemos que:
Pasar un dato por valor: se copia el dato de una variable a otra.
Pasar un dato por referencia: se mueve/modifica el dato en la misma variable.
Variable normal: la variable que normalmente usamos.
Variable puntero: es la variable especial que estamos estudiando.
Veamos un ejemplo sencillo usando punteros: #include #use delay (clock=4000000) //******************************* void main (){ int k;
// variable normal
int *p;
// la variable puntero
k=0xfa;
// k 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 #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