UNIVERSIDAD DE ALCALÁ Departamento de Electrónica P ROGRAMACIÓN EN C DEL 8051 José Manuel Villadangos Carrizo Julio Pastor Mendoza Programación en C del 8051 1. INTRODUCCIÓN Cuando se desarrolló el 8051, el lenguaje ensamblador era utilizado para producir código eficiente en aplicaciones en las que el tiempo era crítico. La reducida capacidad de la memoria interna del chip, tanto de datos como de programa, hacía que el código generado por los primeros compiladores de alto nivel que salieron al mercado, no fuese muy eficiente. Hoy en día existen varios fabricantes de software, que han desarrollado compiladores de alto nivel capaces de generar código en C tan eficiente como aquellas partes de programa que sean necesarias escribir en ensamblador. Trataremos de demostrar las ideas de programación en C del 8051 a partir del compilador de Franklin C51, del cual es fácil disponer de un versión de evaluación (www.fsinc.com) que aunque está limitada a generar un tamaño máximo de código de 4 Kbytes, es suficiente para llevar a cabo numerosas aplicaciones de interés, y sobre todo para conocer un entorno integrado de programación y depuración en C totalmente profesional, para la familia MCS-51 de Intel. Después de su aparición en 1978, el C supuso un gran cambio para los programadores debido a las ventajeas que tenía frente a otros lenguajes de alto nivel. En diciembre de 1989, el Instituto Nacional de Standards de América (ANSI) definió formalmente el standard del lenguaje C, conocido como ANSI C. La mayoría de los compiladores se rigen por este standard, y algunos contienen extensiones al ANSI C para solucionar generalmente problemas de direccionamiento, relacionados con aplicaciones especificas. El lenguaje C tiene la capacidad de generar de manera rápida, comandos de bajo nivel para acceder directamente a la memoria de datos y periféricos, mecanismos para combinar datos Laboratorio de Sistemas Electrónicos Digitales II Pág. 1 Programación en C del 8051 simples de tamaño byte y word en estructuras complejas de datos que permiten accesos rápidos, y herramientas para crear funciones complejas de comandos simples de bajo nivel. también tiene una gran potencia para el control de estructuras, operadores, y librerías de funciones para llevar a cabo operaciones de alto nivel y programación estructurada. El C se desarrolló sin pensar indudablemente en el 8051. Un microcontrolador es básicamente un microprocesador especializado, con numerosos periféricos internos y pensado para aplicaciones de control. Por esta razón, los programadores que habitualmente programan en C, necesitan habituarse con el estilo de programación en C para el 8051, debido a las restricciones y extensiones que existen para el 8051. 2. ORGANIZACIÓN DE LA MEMORIA EN EL 8051 El 8051 tiene un espacio separado de memoria de programa y datos, dependiendo de la configuración particular del sistema, y también puede tener un direccionamiento interno o externo de la memoria de programa y un sólo un área interna, o interna y externa de la memoria de datos. En un sistema típico basado en el 8051, pueden existir por tanto, tres espacios de memoria, todos ellos comenzando a partir de la dirección cero. Además todo esto se complica por la estructura de la RAM interna, donde los primeros 128 bytes son utilizados por el banco de registros, el area de direccionamiento bit a bit, y se completan con posiciones de memoria de propósito general. A esto hay que añadir el segundo área de 128 bytes donde se localiza la zona de registros de función especial (SFR). Además en el caso del 8052, existe un nuevo área de 128 bytes que se solapa con la zona SFR, y que sólo permite un direccionamiento indirecto, para distinguir los accesos de la zona SFR a la que sólo se permite un direccionamiento directo. Dada esta estructura de la memoria tan compleja, existen 6 tipos de especificadores de memoria cuyos márgenes se detallan a continuación: code Memoria de programa (64 Kbytes). data Memoria de datos direccionable directamente para permitir accesos a variables más rápidos, dentro de los primeros 128 bytes de RAM interna. idata Memoria de datos con direccionamiento indirecto a los 256 bytes de memoria RAM interna (en el 8052). bdata Área de datos (16 bytes) con direccionamiento bit a bit, para permitir accesos tipo bit a variables de carácter y enteras (128 bits). xdata Memoria de datos externa (64 Kbytes). pdata Memoria de datos externa paginada, para permitir accesos a los primeros 256 bytes (página 0) a través del puerto 0. A continuación se muestran algunas sentencias en C, para especificar que las variables x e y Laboratorio de Sistemas Electrónicos Digitales II Pág. 2 Programación en C del 8051 residan en la RAM interna y sean direccionadas directamente, y que el array buffer[100] se sitúe en RAM externa. char data x, y; unsigned int xdata buffer[100]; En aplicaciones de control es habitual situar ciertas constantes en una tabla dentro de la memoria de programa (ROM). Esto se define fácilmente, con la siguiente sintaxis: unsigned char code parametros[]={0x10, 0x20, 0x40, 0x80}; La selección del tipo de memoria para las diferentes variables afecta directamente a la eficiencia del programa final. Como regla general, se usa el área de memoria de tipo data para almacenar las variables que requieren un acceso rápido y frecuente. El tipo idata resulta más pequeño que el tipo data debido al empleo de direccionamiento indirecto. Se recomienda su empleo con arrays o estructuras de pequeño tamaño. Todas las variables que no requieran un acceso crítico en el tiempo, y aquellos arrays y estructuras de tamaño grande son propias de residir en el área de tipo xdata. Pueden existir variables cuyos tipos de memoria no sean declarados explícitamente mediante los especificadores tipo, o puede que no se desee especificar el tipo de memoria de varias variables porque todas ellas residan en el mismo área de memoria. También, nuestro programa puede ser pequeño y por tanto residir en la memoria interna del chip. En estos casos, las siguientes definiciones de modelos de memoria permiten elegir, o un tipo de memoria para todo el programa (o función), o un tipo de memoria por defecto para todas aquellas variables que no se han declarado explícitamente. small Las variables y los datos locales se definen para residir en la memoria de datos interna, de igual forma que si hubiesen sido definidas por el especificador data. compact Las variables y los datos locales se definen para residir en la memoria de datos externa, de igual forma que si hubiesen sido definidas por el especificador pdata. large Las variables y los datos locales se definen para residir en la memoria de datos externa, de igual forma que si hubiesen sido definidas por el especificador xdata. La selección del modelo de memoria puede hacerse o en tiempo de compilación ( opción del software de desarrollo ProView ) o usando la directiva #pragma dentro del código fuente. El modelo de memoria por defecto es small. Todas las variables automáticas de una función pueden forzarse al definir la función, a residir en un área de memoria particular. Por ejemplo la función: int func(int x, int y) large { x=10; Laboratorio de Sistemas Electrónicos Digitales II Pág. 3 Programación en C del 8051 y=20; printf(“La suma es %d\n”, x+y); return (x-y+5); } sitúa sus variables locales en memoria externa debido a que el modelo de memoria se ha declarado como large. Una forma de seleccionar globalmente un tipo de memoria para todo el programa es utilizando la directiva #pragma COMPACT al comienzo del programa, y decidir cuidadosamente que modelos de memoria son apropiados, para aquellas funciones individuales y rutinas de interrupción. Aquellas variables que son declaradas explícitamente mediante los especificadores de tipo de memoria, no se ven afectadas por el modelo de memoria seleccionada. 3. CONSTANTES, VARIABLES, Y TIPOS DE DATOS Los programas se escriben o para procesar datos, ya sean recibidos por alguna entrada. El compilador C51 de Franklin soporta los siguientes tipos de datos: Tabla 1. Tipos de datos soportados por C51 Tipo de datos Bits Bytes signed char unsigned char enum signed short unsigned short signed int unsigned int signed long unsigned long float bit sbit sfr sfr16 8 8 16 16 16 16 16 32 32 32 1 1 8 16 1 1 2 2 2 2 2 4 4 4 1 2 Rango de valores -128 a +127 0 a 255 -32768 a +32768 -32768 a +32768 0 a 65535 -32768 a +32768 0 a 65535 -2147483648 a 2147483647 0 a 4294967295 ±1.175494E-38 a ±3.402823E+38 0a1 0a1 0 a 255 0 a 65535 Tal como puede observarse en la tabla, el compilador C51 soporta todos los tipos de datos del standard C. Sin embargo, los tipos de datos señalados en negrita son específicos del 8051 y no forman parte del ANSI C. Por definición, C no soporta accesos directos a datos de tipo bit ni su procesamiento. La forma en que C accede a datos de tipo bit es mediante el uso de máscaras de bit con los correspondientes operadores. Debido a que el 8051 está diseñado para aplicaciones de control, el acceso directo a los bit de información es esencial en su programación. Los datos tipo bit pueden utilizarse para declarar variables tipo bit que residirán en el área de memoria interna de direccionamiento a nivel de bit ( posiciones comprendidas entre la 20H y 2FH de la RAM interna). Por ejemplo, las siguientes declaraciones Laboratorio de Sistemas Electrónicos Digitales II Pág. 4 Programación en C del 8051 bit flag1=0, flag2=0, semaforo; declaran tres variables tipo bit y se inicializan dos de ellas a cero. Los datos tipo sbit se diferencian de los datos tipo bit, en que se usan fundamentalmente para declarar variables tipo bit asociadas con los registros de función especial (SFR). Recordar que la mayoría de los registros de función especial del 8051 son direccionables bit a bit (concretamente aquellos que son divisibles por 8 ). Por ejemplo, la declaración siguiente sbit at 0xAF EA; establece EA como una variable tipo bit con un direccionamiento absoluto, cuya dirección es la 0xAF de la memoria interna. Para el 8051, todos los bit de estado y control de aquellos registros de función especial son predeclarados en el archivo reg51.h. Además, al incluir el archivo reg51.h en nuestro programa, podemos hacer uso de todos los símbolos de bit tales como TR0, TI, ET0, CY, ... Se puede emplear la declaración sbit para acceder a los bit internos de los objetos declarados por el tipo de especificador de memoria bdata . Por ejemplo, las declaraciones: int bdata itemp; sbit bit_ten=itemp^10; permiten que una nueva variable bit_ten, acceder al décimo bit de una variable entera. Los datos tipo sfr se usan para declarar y acceder a los registros de la zona de memoria SFR, aunque tal como ocurría con los datos tipo sbit, todos los registros definidos en el área SFR están predefinidos en el archivo reg51.h. Los siguientes ejemplos muestran como acceder a los puertos y registros internos del 8051: TMOD=0x20; TH1=0xfd; TCON=0x40; IE=0x90; P1=P1&0xf0; 4. ARRAYS, ESTRUCTURAS, Y UNIONES Las variables de los tipos de datos básicos pueden agruparse para formar tipos de datos de nivel superior, con objeto de mejorar la eficiencia del código. 4.1. Arrays Un array es un grupo de variables del mismo tipo, referenciadas con un nombre común. Por ejemplo, la sentencia: int temp[20]; declara un array capaz de almacenar 20 enteros. Las 20 variables de tipo entero se referencian desde temp[0] hasta temp[19]. Laboratorio de Sistemas Electrónicos Digitales II Pág. 5 Programación en C del 8051 A continuación se muestran algunos ejemplos de declaración de arrays: unsigned int pdata temp[40]; float xdata num[20][10]; unsigned char code texto[]=”Hola”; unsigned char code tabla[]={10, 20, 30, 40}; unsigned char code tabla_orden[]= {“Primero”, “Segundo”, “Tercero”, “Cuarto”}; Si el tamaño de un array no está explicitamente definido en la declaración, C automáticamente calcula el tamaño a partir de los datos que se han utilizado en la inicialización. Cuando el tamaño del array sea grande, conviene que sea inicializado en el área xdata. 4.2. Estructuras Una estructura es un conjunto de variables relacionadas, que pueden o no ser del mismo tipo de datos. Por ejemplo: struct alarma{ unsigned unsigned unsigned unsigned }; char dia_string[10]; int hora; int minuto; int segundo; declara una estructura de datos para representar el día y la hora de activación de una alarma en un programa. Con la estructura ya declarada, una variable con ese tipo de estructura sería declarada de la forma siguiente: struct alarma alarma_on; Es importante diferenciar entre la declaración del tipo de datos de la estructura y la declaración de la variable de tipo estructura. La declaración del tipo de datos de la estructura, le dice sólo al compilador el tipo de datos que serán procesados, mientras que la declaración de la variable le dice al ordenador la cantidad de memoria necesaria para almacenar los datos de la estructura en la definición. En el ejemplo anterior, alarma es una estructura formada por un array de caracteres seguido de tres enteros sin signo. Sin embargo, la variable alarma_on de tipo alarma, toma 15 bytes de espacio en memoria. La inicialización de la variable podría ser: struct alarma alarma_lunes={“Lunes”, 12, 30, 00}; Para acceder a las variables de la estructura, se utiliza el operador (.). Veamos un ejemplo: printf(“La activación el %s sera a las %d:%d:%d”, alarma_lunes.dia_string, alarma_lunes.hora, alarma_lunes.minuto, alarma_lunes.segundo); Y el resultado de la ejecución sería: La activación de la alarma el Lunes sera a las 12:30:00 Laboratorio de Sistemas Electrónicos Digitales II Pág. 6 Programación en C del 8051 En aplicaciones de control, suele ser habitual que un grupo de datos sea recogido de la misma fuente pero almacenado en variables de diferente tipo. Una estructura que permita accesos rápidos a las diferentes variables recogidas bajo el mismo nombre simbólico, hace que el programa sea más comprensible. Sin embargo, los bytes de la estructura se almacenan en memoria de forma contigua, y el C51 no permite mezclar variables tipo bit con otros tipos de variables, debido a que los datos tipo bit son restringidos a los 16 bytes de memoria interna que permiten un direccionamiento tipo bit. El tipo de especificador bdata, que declara una variable entera o de carácter para ser direccionada bit a bit, puede utilizarse para forzar a una estructura a ser almacenada dentro del área de direccionamiento de bit: bdata estruct swithches { unsigned char sw1; unsigned char sw2; } swset; sbit sbit sw_read1 = swset.sw1 ^ 0; sw_read2 = swset.sw2 ^ 4; 4.3. Uniones Las uniones son similares a las estructuras y ambas son utilizadas para agrupar variables de diferente tipo bajo un nombre común. Sin embargo, mientras que en una estructura las variables se sitúan en memoria de forma contigua, una unión guarda todos sus datos en las mismas posiciones, solapandose unos con otros, es decir, que ocupan las mismas posiciones pero no al mismo tiempo. Una unión tiene aplicación cuando parte de un dato se trata de forma diferente, en diferentes instantes de tiempo. Supongamos que queremos almacenar el valor de un timer de 16 bits, para leerlo como dos bytes. Como ejemplo, podemos definir una unión constituída por una estructura de 2 bytes y un entero. Cuando se quiere escribir en el byte alto, referenciamos dicho espacio como 2 bytes, pero cuando queremos usar el resultado lo consideramos como un entero. union split{unsigned int word; struct {unsigned char hi;unsigned char low;} bytes}; Declaremos la variable new_count tipo union, y veamos la forma de utilizarla: union split new_count; new_count.bytes.hi = TH1; new_count.bytes.low = TL1; old_count=new_count.word; 5. PUNTEROS La potencia de los punteros en C, es ya conocida. El concepto de puntero es simplemente una variable especial que guarda la dirección de otras variables o constantes. Por ejemplo, en lenguaje Laboratorio de Sistemas Electrónicos Digitales II Pág. 7 Programación en C del 8051 ensamblador del 8051, las funciones principales de los registros R0, R1, DPTR, y SP son almacenar las direcciones de los datos para ser accedidos mediante direccionamiento indirecto, y por tanto, pueden ser definidos como punteros a los datos direccionados. La principal utilización de los punteros es el direccionamiento indirecto de los datos. Como todas las variables residen en RAM interna o externa, pueden ser accedidas mediante el uso de punteros. Se utilizan normalmente en la indexación, direccionamiento y movimiento de datos. Una variable px se declara como variable puntero, de la forma siguiente: char *px y sirve para apuntar a datos de tipo carácter. Por definición, las variables puntero son siempre de tipo unsigned int, pues guardan sólo direcciones. Actualmente, el tipo de datos de los punteros depende del espacio de memoria de la tarjeta y del compilador utilizado. No hemos de confundir la dirección de la variable.... El ejemplo siguiente resume los conceptos de indirección de los punteros: void main (void) { char x, *px; px = &x; x = ‘d’; printf(“El puntero px apunta a %c”, *px); } Este programa imprimiría el carácter contenido en la variable x, es decir, d. Con px apuntando a x, el programa es capaz de acceder indirectamente al contenido de x. También existe otra forma de inicializar la variable puntero: char x; char *px = &x; que asigna a px la dirección de x (no a *px !). 5.1. Tipos de punteros en el 8051 Cuando se declara un puntero, se necesita especificar no sólo dónde el puntero va a ser almacenado, sino también a qué tipo de zona de memoria va a apuntar. La declaración siguiente unsigned char data * xdata x_ptr = 0x4a; Laboratorio de Sistemas Electrónicos Digitales II Pág. 8 Programación en C del 8051 declara a x_ptr como una variable puntero que residirá en memoria xdata, para apuntar a los datos o variables de tipo carácter almacenados en la memoria de tipo data. El puntero x_ptr es inicializado con el valor 0x4a, que ha de ser una dirección de tamaño byte para apuntar dentro del área de memoria tipo data. En la figura 1 puede verse como x_ptr apunta a una posición de memoria del área data. La variable puntero consta de 3 bytes, uno para almacenar un número índice y otros dos para almacenar la dirección sin signo 4Ah. Observe las posiciones de los dos tipos de especificadores de memoria de la declaración anteriormente vista: xdata se sitúa inmediatamente antes de la variable puntero y especifica el tipo de memoria donde se sitúa el puntero. El especificador data seguido del asterisco (*) le dice al compilador que este puntero se usa para apuntar los datos ubicados en el área data de memoria. Los punteros así declarados reciben el nombre de punteros tipo porque los tipos de memoria están explícitamente declarados. Se puede resumir diciendo que un puntero tipo queda declarado bajo la siguiente sintaxis: dato_tipo mem_tipo_d * [mem_tipo_ptr] ptr_nombre [ = dirección]; dato_tipo: Tipo de dato a ser apuntado, y puede ser uno de los habitualmente utilizados: char, int, float, ... mem_tipo_d: Tipo de especificador de memoria que define el espacio de memoria donde los datos van a ser apuntados. Puede ser de tipo code, data, idata, pdata, o xdata. mem_tipo_ptr: Tipo de especificador de memoria (opcional) que define el espacio de memoria donde reside el puntero. Puede ser de tipo code, data, idata, pdata, o xdata. En el caso que no se defina, el tipo de memoria seleccionado es el que exista por defecto. ptr_nombre: N o m b r e asignado a la variable puntero. dirección: Opcionalme n t e , constituye el valor de dirección con el que se inicializa. Un puntero data puede tomar valores entre 0x00 y 0x7F; Laboratorio de Sistemas Electrónicos Digitales II Tipo de memoria donde reside el puntero 4 4A 4B 4A x_ptr xxxxxxxx 49 xdata data Fig.1. Punteros Pág. 9 Programación en C del 8051 un puntero idata, entre 0x00 y 0xFF; un puntero xdata, entre 0x0000 y 0xFFFF; un puntero pdata, entre 0x00 y 0xFF; y un puntero code, entre 0x0000 y 0xFFFF Algunos ejemplos de declaración de punteros son: unsigned char xdata * data s_ptr = 0x1234; int code * tabla_ptr = 0x1000; unsigned float xdata * xdata u_ptr; El compilador de C de Franklin permite operar con los denominados punteros sin tipo, o también denominados genéricos, que se utilizan para acceder a cualquier variable, sin importarnos su posición en la memoria del 8051. Varias de las librerías del C51 utilizan este tipo de puntero, pues mediante estos punteros genéricos, una función puede acceder a los datos sin tener en cuenta el lugar de memoria donde están almacenados. Se declaran de igual forma a como especifica el ANSI C: char *p; int *ptr; long *x; Los punteros genéricos siempre se almacenan en tres bytes. El primero de ellos es el que especifica el tipo de memoria, el segundo es el byte alto que representa a la dirección, y el tercero es el byte bajo. La tabla 2 muestra los valores del byte que identifica al tipo de memoria: Tabla 2. Identificación de la memoria del puntero Tipo de memoria Valor idata xdata pdata data/bdata code 1 2 3 4 5 Cuando a un puntero genérico se le asigna la dirección de la variable después de su declaración, por ejemplo: int xdata x; ptr = &x; el primer byte de ptr, representa al tipo de memoria de la variable x. Como x se ha definido en memoria xdata, el primer byte toma el valor 2. Los siguientes dos bytes de ptr, constituyen la dirección. Un puntero genérico a xdata localizado en la memoria por defecto, puede inicializarse con una dirección xdata de forma directa: unsigned char * ptr = 0x24536L; donde el valor 2, representa al tipo de memoria xdata que se toma por defecto (primer byte de ptr) y el sufijo L inicializador long. Tal declaración es equivalente a haber escrito: unsigned char xdata * ptr = 0x4536; Laboratorio de Sistemas Electrónicos Digitales II Pág. 10 Programación en C del 8051 El código que resulta al emplear punteros declarados de forma genérica es más lento que el equivalente considerando la declaración bajo un puntero tipo. Esto se debe a que el área de memoria no se conoce hasta el tiempo de ejecución. El compilador no puede optimizar los accesos a memoria y produce un código genérico que puede acceder a cualquier posición de memoria. Puede especificarse el área de memoria en la que se almacenará un puntero genérico, mediante el uso del especificador de tipo de memoria: char * xdata sptr; int * data nptr; long * idata vptr; Las anteriores declaraciones son punteros a variables que pueden almacenarse en cualquier área de memoria. Sin embargo, los punteros se almacenan en xdata, data e idata respectivamente. 5.2. Conversión de punteros El compilador C51 de Franklin permite conversiones entre punteros tipo y punteros genéricos. La conversión de punteros puede forzarse explícitamente en el programa usando un molde (type cast) o puede ser forzado por el compilador. Por ejemplo, el compilador C51 fuerza un puntero tipo en un puntero genérico cuando el puntero tipo se pasa como argumento de una función que requiere un puntero genérico. Este es el caso de funciones tales como printf, sprintf, y gets que utilizan punteros genéricos como argumentos. Un puntero tipo usado como argumento de una función, se convierte siempre a un puntero genérico si no está presente en el prototipo de la función. Esto puede causar errores si la llamada a la función espera un puntero más corto como argumento. Con objeto de evitar este problema, se recomienda utilizar ficheros include y prototipos en todas las funciones. Esto garantiza la conversión de los tipos por el compilador e incrementa la probabilidad de que al compilar se detecten errores de conversión. En el menú de ayuda del entrono de desarrollo ProView de Franklin, se pueden observar los tipos de conversión entre punteros. Los ejemplos siguientes ilustran algunas conversiones de punteros y el código resultante: int int int int *ptr1; xdata *ptr2; idad *ptr3; code *ptr4; /* /* /* /* void pconv(void) { ptr1 = ptr2; ptr1 = ptr3; ptr1 = ptr4; ptr4 = ptr1; ptr3 = ptr1; ptr2 = ptr1; ptr2 = ptr3; puntero puntero puntero puntero // // // // // // // genérico (3 bytes) */ xdata (2 bytes) */ idata (1 byte) */ code (2 bytes) */ xdata * a genérico * idata * a genérico * code * a genérico genérico * a code * genérico * a idata * genérico * a xdata * idata * a xdata * Laboratorio de Sistemas Electrónicos Digitales II Pág. 11 Programación en C del 8051 ptr3 = ptr4; } // code * a idata * 5.2. Puntero a arrays El nombre de cualquier array es un puntero fijo (constante puntero) que apunta al primer elemento del array. Por ejemplo, un array de enteros temp[20], consta de los elementos temp[0], temp[1], ... temp[19]. Entonces, temp, que es el nombre del array es un puntero a una constante que es la dirección de temp[0], y por tanto, no puede modificarse. No es legal temp++, al igual que tampoco lo es 5++. Una contante puntero puede utilizarse en un programa al igual que cualquier otra constante, pero ha de seguir ciertas reglas. Por ejemplo, temp+1 apunta al siguiente elemento del array, es decir a temp[1], y no a la siguiente posición de memoria donde el array está almacenado. Esto es debido a que cada elemento del array requiere varios bytes de memoria. De ahí que podamos acceder a los elementos del array ya sea utilizando la notación temp[n], o *(temp+n). Además, el operador de dirección puede también ser utilizado para direccionar cualquier elemento del array. Por ejemplo, &temp[2] es la dirección del tercer elemento del array, temp[20]. Considerando arrays de mayor dimensión, las reglas son las mismas. Así, para el caso de un array bidimensional, *(*(temp+n)+m) es equivalente a temp[n][m]. Una variable puntero puede crearse para servir de índice y acceder a los elementos de un array, al mismo tiempo que permite ser manipulada fácilmente con ayuda de los diferentes operadores de C. El ejemplo siguiente muestra como una variable puntero se utiliza para acceder a los elementos de un array por indexación. #include <stdio.h> void main(void) { int temp[3]; int sum=0, sum_celsius=0; int num, dia=0; int *ptemp; printf(“\n”); ptemp = temp; do { printf(“Introduce para el dia %d: “, ++dia) scanf(“%d”, ptemp); } while ( *(ptem++) > 0 ); ptemp = temp; num = dia-1; for (dia=0; dia<num; dia++) { sum += *(ptemp+dia); sum_celsius += 5*(*(temp+dia)-32)/9; } printf (“El valor medio de la temperatura es %d en Fahrenheit y %d en Celsius”, sum/num, sum_celsius/num); Laboratorio de Sistemas Electrónicos Digitales II Pág. 12 Programación en C del 8051 } El programa toma hasta 31 valores de temperatura positivos e imprime en pantalla el valor medio. Un uso común de los punteros a arrays es la declaración de un array de caracteres mediante una variable puntero. char *parray = {”el sol, el mar, y la tierra”}; 5.3. Arrays de punteros Un grupo de variables puntero pueden formar un array de punteros, que de forma simultánea apuntan a diferentes elementos en un grupo de datos, de manera similar a un array bidimensional. Una aplicación de un array de punteros es en la indexación de un grupo de mensajes para ser mostrados por un display, y acceder de forma rápida con referencias de unos a otros. A continuación se muestra un programa ejemplo de como un array de punteros ordena un grupo de nombres. #include <stdio.h> #include <conio.h> #include <string.h> #define MAXNUM 30 #define MAXLEN 30 void ordena(char *, int); char nombre[MAXNUM][MAXLEN]; void main (void) { char *temp; int cuenta=0; int in, out; clrscr(); printf(“\nIntroduce 8 nombre. Pulsa return para finalizar la entrada\n”); while (cuenta < MAXNUM) { printf (“Nombre %d: “, cuenta+1); gets(nombre[cuenta]); if (strcmp(nombre[cuenta++], “”)==0) break; } ordena(nombre[0],cuenta-1); printf(“\n\n Pulsa una tecla para comenzar.”); getch(); } void ordena(char *pp, int cnt) { char *kp, *pt[MAXNUM]; int x, y; for(x=0; x<MAXNUM-1; x++) Laboratorio de Sistemas Electrónicos Digitales II Pág. 13 Programación en C del 8051 pt[x]=pp+40*x; for(x=0; x<cnt-1; x++) for(y=x+1; y<cnt; y++) if (strcmp(pt[x],pt[y]) > 0) { kp = pt[y]; pt[y] = pt[x]; pt[x] = kp; } printf(“\nLista ordenada: \n”); for (x=0; x<cnt; x++) printf(“\nNombre %d: %s”, x+1, pt[x]); } Observe que la función ordena(), indexa los nombres almacenados en el array con array de puntero. Un array de punteros puede inicializarse para apuntar a un grupo de cadenas directamente. Por ejemplo, char *nombres[]= {”Jose”, “Julio”, “Antonio”, “Ana” }; El array declarado podría apuntar a cada cadena interna. Esto es, nombres[0] es un puntero a la dirección de la primera cadena, nombres[1] a la dirección de la segunda, y así sucesivamente. Las cadenas agrupadas según esta estructura ocupan menos memoria, mientras que si se hubiese empleado un array de dimensión 4*10, se necesitaría rellenar los caracteres no completados para cada nombre. 5.4. Punteros a estructuras y uniones Un puntero puede también apuntar a una estructura, de modo que se pueda acceder a cualquier miembro de la estructura. La sintaxis se muestra a continuación: struct medida { int tiempo; int retardo; char patron; }; struct medida x, *ptr; ptr = &x; Se ha declarado un tipo de estructura llamado medida y definido una variable puntero ptr que apunta a la variable x de tipo estructura. El acceso a los elementos de la estructura se lleva a cabo con el operador ->. ptr->retardo = 30; ptr->patron = 0x40; Laboratorio de Sistemas Electrónicos Digitales II Pág. 14 Programación en C del 8051 donde el valor 30 se sitúa en la variable entera tiempo, y el dato 0x40 en la variable carácter patron. Además, en el caso de arrays de caracteres, C permite la declaración de una varible de tipo estructura, sin el nombre de la variable. struct medida { int tiempo; int retardo; char patron; }*ptr; En este caso el acceso a los elementos de la estructura es idéntico al visto anteriormente. Una unión es similar a una estructura, de ahí que se use el mismo formato en su declaración: union medida { int tiempo; int retardo; char patron; }*ptr; 6. SENTENCIAS DE CONTROL Se detallan a continuación las diferentes sentencias de control que se pueden emplear en un programa en C. Se utilizan para realizar bucles y mecanismos de decisión para controlar el flujo del programa. Permiten que una sentencia de C o un grupo de ellas puedan ejecutarse de manera selectiva, o repetirse un número determinado de veces, o que sean ejecutadas si se cumple una determinada condición dentro del programa. 6.1. Sentencias while y do...while En aplicaciones de control, es habitual monitorizar una determinada entrada o registro, mientras se lleva a cabo una determinada tarea. La sentencia while puede utilizarse para llevar a cabo dicha monitorización. En el ejemplo siguiente, while (P1&0x20) { x=P1; P1=0x00; P2=0x0C; } las sentencias que se encuentran entre las llaves, se ejecutan sólo si el bit 5 del puerto 1 es un “1". La forma de esperar a que suceda una determinada señal para continuar con la ejecución del programa, es utilizando la siguiente sentencia: while (semaforo); Se trata de un bucle infinito, mientras el valor de semaforo permanezca a nivel alto. La sentencia do-while es similar a while salvo que la condición se evalúa después de haberse ejecutado el código que resida dentro del cuerpo. Esto trae como consecuencia que el bucle se Laboratorio de Sistemas Electrónicos Digitales II Pág. 15 Programación en C del 8051 ejecuta siempre al menos una vez. do { una o más sentencias de C; } while (expresión); 6.2. Sentencia if...else La sentencia if constituye la manera más simple de tomar una decisión en C. Tiene la sintaxis general: if (test) sentencia1; else sentencia2; Donde test es una expresión cuyo resultado se evalúa como cierto (distinto de cero) o falso (igual a cero). Si la expresión test es cierta se ejecuta la sentencia1 y si es falsa la sentencia2. En la expresión que condiciona la ejecución, pueden emplearse los operadores lógicos y de igualdad. if (++conta == limite){ if(!CY) { set_alarma(); flag=1; } } else x=P1; Observe en el ejemplo anterior que existe una nueva sentencia if, dentro de la primera. Sólo si la condición más externa es cierta, se entra en el segundo if. No debe confundir el operador de igualdad “==” con el de asignación “=”. La palabra clave else se puede combinar con la palabra clave if para generar secuencias anidadas. if (var=10) printf(“La variable es 10"); else if (var==20) printf(“La variable es 20); else printf(“La variable no es ni 10 ni 20); 6.3. Sentencia for Tiene la sintaxis general: for (expresión_inico, expresión_test; expresión_repetición) { una o más sentencias de C; } Esta sentencia al igual que while consiste en la repetición de un bucle. Dentro del for, se debe Laboratorio de Sistemas Electrónicos Digitales II Pág. 16 Programación en C del 8051 indicar el valor inicial de la expresión, la condición que debe cumplirse para finalizar el bucle y qué modificaciones deben realizarse en cada iteración. void main (void) { int n, factorial=1; for(n=6;n==1;n--) factorial*=n; } 6.4. Sentencias switch, case, break y default La sintaxis es la siguiente: switch (num) { case 1: sentencia1; break; case 2: sentencia2 break; defaul: otras_sentencias; } Esta sentencia permite ejecutar una serie de sentencias en función del valor de una variable. En el ejemplo anterior la variable a consultar es num y los valores que se contemplan son 1 y 2. En caso de que la variable valga 1 se ejecutaría la sentencia1, después de lo cual con la sentencia break el flujo del programa se va a la línea siguiente del final del bucle switch. Si num es 2 se ejecuta la sentencia2, y si no fuese ninguno de los dos valores iría a default y ejecutaría otras_sentencias. 6.5. Sentencia continue La sentencia continue provoca el salto de control del programa desde el lugar en que se encuentre hasta el final del bloque en curso, saltando todas las sentencias que haya entre ellas, pero permitiendo que el bucle continúe. 7. FUNCIONES, MÓDULOS Y PROGRAMAS Las funciones permiten construir bloques de un programa. Un programa en C es un conjunto de funciones especialmente diseñadas para realizar ciertas tareas específicas, y en conjunto llevar a cabo el objetivo final del programa. Generalmente una función se crea para realizar una determinada tarea por sí misma, y comunicarse con otras funciones ya sea mediante el paso de parámetros que ha de recibir como entrada para ser procesados, o devolviendo algún resultado que otras funciones precisen. Numerosas tareas básicas pueden implementarse a través de funciones y así ser utilizadas en diferentes programas, o dentro de un mismo programa en diferentes instantes de tiempo. Laboratorio de Sistemas Electrónicos Digitales II Pág. 17 Programación en C del 8051 Cuando una función se llama, el control se pasa a la misma para su ejecución y cuando esta finaliza, el control es devuelto de nuevo al módulo que llamó, para continuar con la ejecución del mismo, a partir de la sentencia siguiente a la que se efectuó la llamada. 7.1. Declaración de una función (prototipos) Teniendo en cuenta la convención que existe en las funciones escritas en C, para declarar una función es necesario es necesario especificar el prototipo de la función, es decir, el nombre de la función, los argumentos, y el tipo de datos que pudiese retornar. También puede ocurrir que se desee modificar el modelo de memoria por defecto para una función particular, con objeto de que tenga unas determinadas características particulares. Para especificar un modelo de memoria para una función, es necesario que las variables locales y los argumentos de la función se almacenen en el área de memoria especificada. Como ya se indicó, la memoria RAM interna del 8051, especificada bajo el modelo small, es buena para guardar datos que necesiten accesos rápidos. Aquellas funciones que tengan tiempos de ejecución críticos, deberían utilizar el modelo de memoria small. Cuando la memoria interna del 8051 no sea suficiente para almacenar datos, dada su pequeña capacidad, será necesario entonces elegir un modelo de memoria adecuado. El 8051 tiene cuatro bancos de registros en RAM interna, cada uno de ellos formado por ocho registros (R0-R7). El banco seleccionado po defecto después del reset es el banco 0. Existe la posibilidad de seleccionar el banco de trabajo por medio de la directiva REGISTERBANK. Esto permite seleccionar un banco diferente para una determinada función. La conmutación de los bancos es habitual al trabajar con interrupciones o en sistemas de tiempo real, a fin de minimizar el tiempo empleado en salvar los registros de interés al entrar en la función. El formato standard para declarar una función de C, en el software de Franklin es: return_tipo [using] nombre_func(arg) [mem_tipo] [reentrant] [interrupt] return_tipo: Tipo del valor retornado por la función. Si no se especifica, se asume por defecto el tipo int. nombre_func: Nombre de la función. args: Es la lista de argumentos de la función. mem_tipo: Especificación explícita del modelo de memoria (small, compact, o large) reentrant: Indica que la función es recursiva o reentrante. interrupt: Indica que se trata de una función de interrupción. using: Especifica el banco de registros utilizado por la función (ej. using Laboratorio de Sistemas Electrónicos Digitales II Pág. 18 Programación en C del 8051 2). A continuación se muestra un ejemplo con diferentes modelos de memoria, y el uso del banco 3 de registros en la función xfunc(). #pragma small void main (void) { int i=10, j=20, k; k= xfunc(i, j); } int xfunc(int x, int y) large using 2 { int z; z=x*y; return(z-x-y); } Las variables locales (i, j, k) de main() se guardan en memoria RAM interna, debido a la directiva #pragma small. Sin embargo, los valores i y j pasados a la función xfunc(), se localizan en la memoria xdata, debido a que los argumentos y variables locales (x, y, z) de la función xfunc() se les aplica el modelo de memoria large. Además, el banco de registros utilizado por la función xfunc(), es el banco 2. 7.2. Paso de parámetros a una función La comunicación entre las funciones se lleva a cabo a través de los argumentos que se usan en la llamada a la función y el valor retornado por la función. Una función es un bloque de instrucciones que residen en memoria de código. Las variables son, por otro lado, posiciones de memoria que pueden ser de cualquiera de los tipos: small, compact, o large. Tradicionalmente en los microprocesadores de 8 bits, los valores pasados a una función eran almacenados en la pila (stack), la cual tenía un tamaño de hasta 64Kbytes. En el 8051 la pila reside en memoria RAM interna (después del reset el SP se sitúa en la posición 07) y su tamaño máximo es de 128 bytes, pudiendo ser tan pequeña como 64 bytes para algunas versiones de la familia (87C751 de Philips). Debido a estas limitaciones existen diferentes formas de llevar a cabo el paso de parámetros a una función. Paso de parámetros por registros Con compilador C51 de Franklin los parámetros se pasan por defecto, de una función a otra a través de registros, estando limitado el número de ellos a tres. Los parámetros que no pueden ser localizados en registros, se pasan automáticamente a través de posiciones fijas de memoria siendo el modelo de memoria utilizado el que se especifique en la declaración. Los tres parámetros que permiten pasarse por registro siguen ciertas reglas, debido a que existen sólo 8 registros disponibles en cada banco, y el tipo de dato de cada parámetro pueden llegar en cualquier orden. La tabla 3 muestra los registros usados en el paso de parámetros a funciones cuando los argumentos son de tipo diferente. Laboratorio de Sistemas Electrónicos Digitales II Pág. 19 Programación en C del 8051 Tabla 3. Registros usados en paso de parámetros a funciones argumento char/ptr de 1byte int/ptr de 2 bytes long/float ptr genérico 1 2 3 R7 R5 R3 R6,R7 R4,R5 R2,R3 R4-R7 R4-R7 R1-R3 R1-R3 R1-R3 Obviamente, no todos los tres parámetro pueden pasarse por registros. Dependiendo de el tipo de datos de los argumentos, un parámetro que no sea pasado por registro se pasa a través del área de memoria asociada con la función. En el menú de ayuda de ProView32, se puede consultar una lista con la combinación de varios parámetros y el tipo de memoria utilizado cuando se pasan a una función. Una función puede retornar un valor a través de un registro. La tabla 4 muestra los registros usados en función del tipo de dato retornado. Tabla 4. Registros usados para el retorno de valores Tipo de dato Registro char (1 byte) R4 int (2 bytes) R4,R5 ptr genérico (3 bytes) R0,R2,R3 float (4 bytes) R4-R7 double (6 bytes) R2-R7 long double (7 bytes) R1-R7 Paso de parámetros a través de memoria El paso de parámetros a una función por registro podría utilizarse en caso de aplicaciones en las que el tiempo es crítico, debido a que su direccionamiento es más rápido. Sin embargo, no siempre es posible utilizar los registros, pues sólo existen 8 disponibles. Otra limitación está en que no pueden pasarse parámetros tipo bit a una función a través de registros. Por ello, el compilador C51 permite utilizar la memoria para llevar a cabo el proceso. La directiva de control NOREGPARMS hace que sólo se permita el paso de parámetros a través de memoria, desactivandose el modo por defecto (uso de registros). Igualmente, esiste la directiva de control REGPARMS, para activar el modo de paso por registro. El ejemplo siguiente ilustra el uso de estas direstivas: Laboratorio de Sistemas Electrónicos Digitales II Pág. 20 Programación en C del 8051 #include <stdio> #pragma NOREGPARMS extern int bfunc(float, int, char) void main (void) { int i=10, j=20, n, k; float x=3.1416; char ch=65; n=bfunc(x, i, ch); k=xfunc(i, j); printf (“%d, %d “, n, k); } #pragma REGPARMS int xfunc(int x, int y) { int z; z=x*y; return(z-x-y); } 7.3. Funciones reentrantes Una función reentrante es aquella que puede ser llamada simultáneamente por otras funciones o recursivamente por ella misma. Los parámetros pasados a una función de este tipo se almacenan junto con las variables locales de la función en una pila creada a tal efecto para la función. La ubicación de la pila queda determinada por el modelo de memoria definido en la función, pudiendo ser cualquiera de los tres posibles. Una función reeentrante se declara con el atributo reentrant tal como se vio en el apartado 7.1. Por ejemplo, si la función xfunc(int x, int y) del programa anterior necesita ser reentrante, la directiva REGPARMS no podría usarse debido a que las funciones reentrantes no reciben parámetros de registros. La función entonces, se declararía así: int xfunc (int x, int y) large reentrant { int z; z=x*y; return(z-x-y); } Los argumentos x, y, así como la variable local z, se ubicarían en xdata debido a que es donde se genera la pila (stack) para la función. El modelo de memoria elegido para la función determina no sólo donde se guardan las variables, sino también la memoria para el paso de parámetros. Debemos tener precaución cuando queramos situar la pila en memoria interna, ya que puede ser fácilmente desbordar la capacidad de la RAM interna. Al comienzo de la RAM y por encima de la zona de registros, es donde se ubican las variables que se declaran tipo bit, data, e idata, y las variables locales de aquellas funciones que se declaran usando el modelo small de memoria. Por encima de estas posiciones es donde se inicializa el puntero de pila (SP). La pila que se crea automáticamente y se asocia con una función que usa el modelo de memoria small, se localiza en la parte alta de la RAM interna, y crece hacia posiciones más bajas a medida que recibe datos. Laboratorio de Sistemas Electrónicos Digitales II Pág. 21 Programación en C del 8051 Debido al número de veces que puede ser llamada la función, puede dar lugar al overflow de la RAM interna. 7.4. Funciones de interrupción y conmutación de los bancos de registros Una función de interrupción es un elemento importante en aplicaciones de tiempo real con microcontroladores. Cuando ocurre una interrupción el sistema entra en un estado especial, que demanda la ejecución de una rutina particular del programa (función de interrupción) suspendiendose la ejecución en curso, hasta que el servicio de interrupción se completa. El 8051 tiene un número de interrupciones hardware que permiten detectar eventos externos, operaciones de los timers, y el control del puerto serie. El compilador C51 soporta interrupciones y permite escribir en C la rutina asociada con la fuente de interrupción. A partir de el número de interrupción y del banco de registros especificado para la función de interrupción, el compilador genera automáticamente el vector de interrupción así como la entrada y la salida al código que constituye la función de interrupción. El atributo interrupt en la declaración de una función permite especificar que dicha función es de interrupción. El argumento del atributo de la función especifica el número de interrupción asociado con dicha función. Por ejemplo, void tiempo (void) interrupt 3 { TH1=-100; TL1=-100; ET1=1; if (dtime=256) P1=1; dtime++; } donde el número de interrupción especifica que esta función corresponde a la función de interrupción del Timer 1. La correspondencia entre el número de interrupción y el tipo de interrupción se muestra en la tabla 5. Cuando se llama a una función de interrupción, la CPU lleva a cabo las siguientes operaciones: 1. Salva el contenido de los registros ACC, B, DPH, DPL, y PSW en la pila cuando sea necesario. 2. Salva todos los registros de trabajo que serán utilizados por la función de interrupción en la pila. 3. Ejecuta la función de interrupción. 4. Restaura todos los registros de la pila salvados antes de salir de la función de interrupción. 5. Ejecuta la instrucción RETI para finalizar la función. Tabla 5. Números de interrupción Laboratorio de Sistemas Electrónicos Digitales II Pág. 22 Programación en C del 8051 Interrupción Número Externa 0 0 Timer 0 1 Externa 1 2 Timer 1 3 P. Serie 4 Con objeto de emplear el menor tiempo posible en salvar los registros en la pila al entrar a una función de interrupción, se puede cambiar de banco de registros. Para ello, el compilador C51 permite utilizar el atributo using N en la declaración de una función de interrupción, donde el valor N representa al banco seleccionado al entrar en la función. Para el ejemplo anterior, y si se quisiese usar el banco 1 al entrar en la función, la declaración sería: void tiempo (void) interrupt 3 using 1 { TH1=-100; TL1=-100; ET1=1; if (dtime=256) P1=1; dtime++; } Laboratorio de Sistemas Electrónicos Digitales II Pág. 23