Programación en C del 8051 con Franklin

Anuncio
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
Descargar