Professional Documents
Culture Documents
ejemplo,
escrito
para
los
/******************************************************************************
* FileName: main.c
* Purpose:
LED parpadeantwe
*****************************************************************************/
#include "avr_compiler.h"
//****************************************************************************
// delay_ms
//****************************************************************************
void delay_ms(unsigned int t)
{
while(t--)
delay_us(1000);
}
//****************************************************************************
// Funcin principal
//****************************************************************************
int main(void)
{
DDRB = 0x01;
for( ;; )
{
PORTB |= 0x01; // Poner 1 en pin PB0
delay_ms(400); //
PORTB &= 0xFE; // Poner 0 en pin PB0
delay_ms(300);
}
}
No hay que ser muy perspicaz para descubrir lo que hace este programa:
configura el pin PB0 como salida y luego lo setea y lo limpia tras pausas. Es
como hacer parpadear un LED conectado al pin PB0. Parpadea porque el
bloque de while se ejecuta cclicamente.
Los elementos ms notables de un programa en C son las sentencias, las
funciones, las directivas, los comentarios y los bloques. A continuacin, una
breve descripcin de ellos.
Los comentarios
Los comentarios tienen el mismo propsito que en ensamblador: documentar y
adornar el cdigo. Es todo es texto que sigue a las barritas // y todo lo que
est entre los signos /* y */. Se identifican fcilmente porque suelen aparecer
en color verde.
Ejemplos.
// ste es un comentario simple
/*
sta es una forma de comentar varias lneas a la vez.
Sirve mucho para enmascarar bloques de cdigo.
*/
Las sentencias
Un programa en C, en lugar de instrucciones, se ejecuta por sentencias.
Una sentencia es algo as como una mega instruccin, que hace lo que varias
instrucciones del ensamblador.
las #if, #elif, #endif y similares. Fuera de ellas, cada compilador maneja sus
propias directivas y sern tratadas por separado.
Las funciones
Si un programa en ensamblador se puede dividir en varias subrutinas para su
mejor estructuracin, un programa en C se puede componer de funciones. Por
supuesto que las funciones son muchsimo ms potentes y, por cierto, algo
ms complejas de aprender. Por eso ni siquiera el gran espacio que se les
dedica ms adelante puede ser suficiente para entenderlas plenamente. Pero,
no te preocupes, aprenderemos de a poco.
En un programa en C puede haber las funciones que sean posibles, pero la
nunca debe faltar la funcin principal, llamada main. Donde quiera que se
encuentre, la funcin main siempre ser la primera en ser ejecutada. De hecho,
all empieza y no debera salir de ella.
Variables y Tipos de Datos
En ensamblador todas las variables de programa suelen ser registros de la RAM
crudos, es decir, datos de 8 bits sin formato. En los lenguajes de alto nivel
estos registros son tratados de acuerdo con formatos que les permiten
representar nmeros de 8, 16 32 bits (a veces ms grandes), con signo o sin
l, nmeros enteros o decimales. Esos son los tipos de datos bsicos.
Las variables de los compiladores pueden incluso almacenar matrices de datos
del mismo tipo (llamadas arrays) o de tipos diferentes (llamadasestructuras).
Estos son los tipos de datos complejos.
Los siguientes son los principales tipos de datos bsicos del lenguaje C.
Observa que la tabla los separa en dos grupos, los tipos enteros y los tipos
de punto flotante.
Tabla de variables y tipos de datos del lenguaje C
Tipo de dato
Tamao en bits
Rango de
adoptar
valores
char
signed char
-128 a 127
unsigned char
0 a 255
(signed) int
16
-32,768 a 32,767
unsigned int
16
0 a 65,536
que
puede
Tamao en bits
Rango de
adoptar
valores
que
puede
(signed) short
16
-32,768 a 32,767
unsigned short
16
0 a 65,536
(signed) long
32
-2,147,483,648 a 2,147,483,647
unsigned long
32
0 a 4,294,967,295
64
-263 a 263 - 1
64
0 a 264 - 1
float
32
1.18E-38 a 3.39E+38
double
32
1.18E-38 a 3.39E+38
double
64
2.23E-308 a 1.79E+308
Tamao en bits
int8_t
-128 a 127
uint8_t
0 a 255
int16_t
16
-32,768 a 32,767
uint16_t
16
0 a 65,536
int32_t
32
-2,147,483,648 a 2,147,483,647
uint32_t
32
0 a 4,294,967,295
int64_t
64
-263 a 263 - 1
Tamao en bits
64
signed char c;
int i;
signed int j;
unsigned int k;
Tambin es posible declarar varias variables del mismo tipo, separndolas con
comas. As nos ahorramos algo de tipeo. Por ejemplo:
float area, side;
// Declarar constante a
// Declarar variable b
//...
b = a;
b = 150;
a = 60;
a = b;
// Vlido
// Vlido
// Error! a es constante
// Error! a es constante
Por ms que las variables constantes sean de solo lectura, ocuparn posiciones
en la RAM del microcontrolador. En CodeVisionAVR es posible configurar para
que s residan en FLASH pero por compatibilidad se usa muy poco. Por eso
muchas veces es preferible definir las constantes del programa con las clsicas
directivas #define (como se hace en el ensamblador).
#define a 100
// Definir constante a
Sentencias selectivas
Llamadas tambin sentencias de bifurcacin, sirven para redirigir el flujo de un
programa segn la evaluacin de alguna condicin lgica.
Las sentencias if e ifelse son casi estndar en todos los lenguajes de
programacin. Adems de ellas estn las sentencias ifelse escalonadas
y switchcase.
La sentencia if
La sentencia if (si condicional, en ingls) hace que un programa ejecute una
sentencia o un grupo de ellas si una expresin es cierta. Esta lgica se describe
en el siguiente esquema.
// Si expression es verdadera,
// apertura de bloque
sentenciaB;
sentenciaC;
// algunas otras sentencias
// cierre de bloque
sentenciaX;
Despus de ejecutar sentenciaA el programa evala expression. Si resulta ser
verdadera, se ejecutan todas las sentencias de su bloque y luego se ejecutar
la sentenciaX.
En cambio, si expression es falsa, el programa se saltear el bloque de if y
ejecutar sentenciaX.
La sentencia if else
La sentencia if brinda una rama que se ejecuta cuando una condicin lgica es
verdadera. Cuando el programa requiera dos ramas, una que se ejecute si
cierta expression es cierta y otra si es falsa, entonces se debe utilizar la
sentecia if else. Tiene el siguiente esquema.
// este bloque
sentenciaB;
sentenciaC;
// ...
}
else
{
sentenciaM;
sentenciaN;
// ...
}
sentenciaX;
// ...
Como ves, es bastante fcil, dependiendo del resultado se ejecutar uno de los
dos bloques de la sentencia if else, pero nunca los dos a la vez.
La sentencia if else if escalonada
Es la versin ampliada de la sentencia if else.
En el siguiente boceto se comprueban tres condiciones lgicas, aunque podra
haber ms. Del mismo modo, se han puesto dos sentencias por bloque solo
para simplificar el esquema.
if ( expression_1 )
{
// este bloque
sentencia1;
sentencia2;
}
else if ( expression_2 )
{
}
else if ( expression_3 )
{
}
else
{
sentencia7;
sentencia8;
};
// ; opcional
// todo...
Las expresiones se evalan de arriba abajo. Cuando alguna de ellas sea
verdadera, se ejecutar su bloque correspondiente y los dems bloques sern
salteados. El bloque final (de else) se ejecuta si ninguna de las expresiones es
verdadera. Adems, si dicho bloque est vaco, puede ser omitido junto con
su else.
La sentencia switch
La sentencia switch brinda una forma ms elegante de bifurcacin mltiple.
Podemos considerarla como una forma ms estructurada de la
sentencia if else if escalonada, aunque tiene algunas restricciones en las
condiciones lgicas a evaluar, las cuales son comparaciones de valores
enteros.
Para
elaborar
el
cdigo
en
reservadas switch, case, break y default.
se
usan
las
palabras
El siguiente esquema presenta tres cases pero podra haber ms, as como
cada bloque tambin podra tener ms sentencias.
switch ( expression )
{
case constante1: // Si expression = constante1, ejecutar este bloque
sentencia1;
sentencia2;
break;
case constante2: // Si expression = constante2, ejecutar este bloque
sentencia3;
sentencia4;
break;
case constante3: // Si expression = constante3, ejecutar este bloque
sentencia5;
sentencia6;
break;
default:
sentencia7;
sentencia8;
break;
}
sentenciaX;
// todo...
donde constante1, constante2 y constante3 deben ser constantes enteras, por
ejemplo, 2, 0x45, a, etc. (a tiene cdigo ASCII 165, que es, a fin de cuentas,
un entero.)
expresion puede ser una variable compatible con entero. No es una expresin
que conduce a una condicin lgica como en los casos anteriores.
El programa solo ejecutar uno de los bloques dependiendo de qu constante
coincida con la expression. Usualmente los bloques van limitados por llaves,
pero en este caso son opcionales, dado que se pueden distinguir fcilmente.
Los bloques incluyen la sentencia break. Qu es eso?
La sentencia break hace que el programa salga del bloque de switch y ejecute
la sentencia que sigue (en el boceto, sentenciaX). Atento!: de no poner break,
tambin se ejecutar el bloque del siguiente case, sin importar si su constante
coincida con expression o no.
No sera necesario poner el default si su bloque estuviera vaco.
Sentencias iterativas
Las sentencias de control iterativas sirven para que el programa ejecute una
sentencia o un grupo de ellas un nmero determinado o indeterminado de
veces. As es, esta seccin no habla de otra cosa que de los bucles en C.
El lenguaje C soporta tres tipos de bucles, las cuales se construyen con las
sentencias while, do while y for. El segundo es una variante del primero y el
tercero es una versin ms compacta e intuitiva del bucle while.
La sentencia while
El cuerpo o bloque de este bucle se ejecutar una y otra vez mientras (while,
en ingls) una expresin sea verdadera.
// siguiente bloque
{
sentenciaB;
sentenciaC;
// ...
};
sentenciaX;
// Este ; es opcional
// ...
Nota que en este caso primero se evala expression. Por lo tanto, si desde el
principio la expression es falsa, el bloque de while no se ejecutar nunca. Por
otro lado, si la expression no deja de ser verdadera, el programa se quedar
dando vueltas para siempre.
La sentencia do - while
Como dije antes, es una variacin de la sentencia while simple. La principal
diferencia es que la condicin lgica (expression) de este bucle se presenta al
final. Como se ve en la siguiente figura, esto implica que el cuerpo o bloque de
este bucle se ejecutar al menos una vez.
La sentencia for
Las dos sentencias anteriores, while y do while, se suelen emplear cuando no
se sabe de antemano la cantidad de veces que se va a ejecutar el bucle. En los
casos donde el bucle involucra alguna forma de conteo finito es preferible
emplear la sentencia for. (Inversamente, al ver un for en un programa,
debemos suponer que estamos frente a algn bucle de ese tipo.)
sta es la sintaxis general de la sentencia for en C:
for ( expression_1 ; expression_2 ; expression_3 )
{
sentencia1;
sentencia2;
// ...
};
// Este ; es opcional
expression_3;
}
No obstante, de esa forma se ve ms rara an; as que, mejor, veamos estos
ejemplos, que son sus presentaciones ms clsicas. (i es una variable
y a y b son constantes o variables):
for ( i = 0 ; i < 10 ; i++ )
{
sentencias;
}
Se lee: para (for) i igual a 0 hasta que sea menor que 10 ejecutar sentencias.
La sentencia i++indica que i se incrementa tras cada ciclo. As, el bloque
de for se ejecutar 10 veces, desde quei valga 0 hasta que valga 9.
En este otro ejemplo las sentencias se ejecutan desde que i valga 10 hasta que
valga 20. Es decir, el bucle dar 11 vueltas en total.
for ( i = 10 ; i <= 20 ; i++ )
{
sentencias;
}
El siguiente bucle for empieza con i inicializado a 100 y su bloque se ejecutar
mientras i sea mayor o igual a 0. Por supuesto, en este caso i se decrementa
tras cada ciclo.
for ( i = 100 ; i >= 0 ; i-- )
{
sentencias;
}
Se pueden hacer muchas ms construcciones, todas coincidentes con la
primera plantilla, pero tambin son menos frecuentes.
Sentencias con bloques simples
Cuando las sentencias selectivas (como if) o de bucles (como while o for)
tienen cuerpos o bloques que constan de solo una sentencia, se pueden omitir
las llaves. Aun as, es aconsejable seguir manteniendo las tabulaciones para
evitarnos confusiones.
if(a == b)
{
a++;
}
else
{
b--;
}
while( a >= b)
{
a = a + b;
}
a = 0;
if(a == b)
a++;
else
b--;
while( a >= b)
a = a + b;
Los operadores
Sirven para realizar operaciones aritmticas, lgicas, comparativas, etc. Segn
esa funcin se clasifican en los siguientes grupos.
Operadores aritmticos
Adems de los tpicos operadores de suma, resta, multiplicacin y divisin,
estn los operadores de mdulo, incremento y decremento.
Tabla de Operadores aritmticos
Operador
Accin
Suma
Resta
Multiplicacin
Divisin
Accin
usar con nmeros enteros.
++
Incrementar en uno
--
Decrementar en uno
Ejemplos:
int a, b, c;
// Declarar variables a, b y c
a = b + c;
b = b * c;
b = a / c;
a = a + c b;
c = (a + b) / c;
// Residuo de dividir ab a c
// Incrementar a en 1
// Decrementar b en 1
// Incrementar c en 1
// Decrementar b en 1
a = b++;
a = ++b;
// algn cdigo
}
// algn cdigo
}
Operadores de bits
Se aplican a operaciones lgicas con variables a nivel binario. Aqu tenemos las
clsicas operaciones AND, OR inclusiva, OR exclusiva y la NEGACIN.
Adicionalmente, he incluido en esta categora los operaciones de
desplazamiento a la derecha y la izquierda.
Si bien son operaciones que producen resultados anlogos a los de las
instrucciones de ensamblador los operadores lgicos del C pueden operar
sobre variables de distintos tamaos, ya sean de 1, 8, 16 32 bits.
Tabla de operadores de bits
Operador
Accin
&
Accin
<<
Desplazamiento a la izquierda
>>
Desplazamiento a la derecha
Ejemplos:
char m;
int n;
// variable de 8 bits
// variable de 16 bits
m = 0x48;
// m ser 0x48
m = m & 0x0F;
m = m | 0x24;
m = m & 0b11110000;
n = 0xFF00;
// n ser 0xFF00
n = ~n;
// n ser 0x00FF
m = m | 0b10000001;
m = m & 0xF0;
m = m ^ 0b00110000;
m = 0b00011000;
m = m >> 2;
n = 0xFF1F;
n = n << 12;
m = m << 8;
Operadores relacionales
Se emplean para construir las condiciones lgicas de las sentencias de control
selectivas e iterativas, como ya hemos podido apreciar en las secciones
anteriores. La siguiente tabla muestra los operadores relacionales disponibles.
Tabla de Operadores relacionales
Operador
Accin
==
Igual
!=
No igual
>
Mayor que
<
Menor que
>=
<=
Operadores lgicos
Accin
&&
AND lgica
||
OR lgica
Negacin lgica
Ejemplos:
if( !(a==0) )
{
// sentencias
}
{
// sentencias
}
// Declarar a
a += 50;
a += 20;
a *= 2;
// Es lo mismo que a = a * 2;
a &= 0xF0;
a <<= 1;
Precedencia de operadores
Una expresin puede contener varios operadores, de esta forma:
b = a * b + c / b;
// a, b y c son variables
{
// ...
}
Las funciones
Una funcin es un bloque de sentencias identificado por un nombre y puede
recibir y devolver datos. En bajo nivel, en general, las funciones operan como
las subrutinas de Assembler, es decir, al ser llamadas, se guarda en la Pila el
valor actual del PC (Program Counter), despus se ejecuta todo el cdigo de la
funcin y finalmente se recobra el PC para regresar de la funcin.
Dada su relativa complejidad, no es tan simple armar una plantilla general que
represente a todas las funciones. El siguiente esquema es una buena
aproximacin.
de
la
funcin.
Puede
ser
Para una funcin que no recibe ni devuelve ningn valor, la plantilla de arriba
se reduce al siguiente esquema:
void function_name ( void )
{
// Cuerpo de la funcin
}
Y se llama escribiendo su nombre seguido de parntesis vacos, as:
function_name();
La funcin principal main es otro ejemplo de funcin sin parmetros.
Dondequiera que se ubique, siempre debera ser la primera en ejecutarse; de
hecho, no debera terminar.
void main (void)
{
// Cuerpo de la funcin
}
// Cambiar a arg2
return min;
// Cambiar a arg3
}
void main (void)
{
int a, b, c, d;
// Declarar variables a, b, c y d
// Bucle infinito
}
En el programa mostrado la funcin minor recibe tres parmetros de tipo int y
devuelve uno, tambin de tipo int, que ser el menor de los nmeros recibidos.
tipo y orden de dichos parmetros. Por eso se deben declarar al inicio del
programa.
El prototipo de una funcin es muy parecido a su encabezado, se pueden
diferenciar tan solo por terminar en un punto y coma (;). Los nombres de las
variables de entrada son opcionales.
Por ejemplo, en el siguiente boceto de programa los prototipos de las
funciones main, func1 yfunc2 declaradas al inicio del archivo permitirn que
dichas funciones sean accedidas desde cualquier parte del programa. Adems,
sin importar dnde se ubique la funcin main, ella siempre ser la primera en
ejecutarse. Por eso su prototipo de funcin es opcional.
#include <avr.h>
void main(void);
void main(void)
{
// Cuerpo de la funcin
// Desde aqu se puede acceder a func1 y func2
}
void func1(char m, long p)
{
// Cuerpo de la funcin
// Desde aqu se puede acceder a func2 y main
}
char func2(int a)
{
// Cuerpo de la funcin
void main(void)
{
// Cuerpo de la funcin
// Desde aqu no se puede acceder a func1 ni func2 porque estn abajo
}
void func1(char m, long p)
{
// Cuerpo de la funcin
// Desde aqu se puede acceder a main pero no a func2
}
char func2(int a)
{
// Cuerpo de la funcin
// Desde aqu se puede acceder a func1 y main
}
Para terminar, dado que los nombres de las variables en los parmetros de
entrada son opcionales, los prototipos de func1 y func2 tambin se pueden
escribir asi
void func1(char, long);
char func2(int );
int speed;
// Prototipo de funcin
// Variable global
void inter(void)
{
int count;
// Variable local
speed++;
vari = 0;
}
void main(void)
{
int count;
speed = 0;
}
char foo(long count)
{
int vari;
}
Algo muy importante: a diferencia de las variables globales, las variables
locales tienen almacenamiento temporal, es decir, se crean al ejecutarse la
funcin y se destruyen al salir de ella. Qu significa eso? Lo explico en el
siguiente apartado.
Si dentro de una funcin hay una variable local con el mismo nombre que una
variable global, la precedencia en dicha funcin la tiene la variable local. Si te
confunde, no uses variables globales y locales con el mismo nombre.
Variables static
Antes de nada debemos aclarar que una variable static local tiene diferente
significado que unavariable static global. Ahora vamos a enfocarnos al primer
caso por ser el ms comn.
Cuando se llama a una funcin sus variables locales se crearn en ese
momento y cuando se salga de la funcin se destruirn. Se entiende por
destruir al hecho de que la locacin de memoria que tena una variable ser
luego utilizada por el compilador para otra variable local (as se economiza la
memoria). Como consecuencia, el valor de las variables locales no ser el
mismo entre llamadas de funcin.
Por ejemplo, revisa la siguiente funcin, donde a es una variable local ordinaria.
void increm()
{
int a;
a++;
// Declarar variable a
// Incrementar a
}
Cualquiera que haya sido su valor inicial, crees que despus de llamar a esta
funcin 10 veces, el valor de a se habr incrementado en 10?... Pues, no
necesariamente. Cada vez que se llame a increm se crea a, luego se
incrementa y, al terminar de ejecutarse la funcin, se destruye.
Para que una variable tenga una locacin de memoria independiente y su valor
no cambie entre llamadas de funcin tenemos dos caminos: o la declaramos
como global, o la declaramos comolocal esttica. Los buenos programadores
siempre eligen el segundo.
Una variable se hace esttica anteponiendo a su declaracin el
especificador static. Por defecto las variables estticas se auto inicializan a 0,
pero se le puede dar otro valor en la misma declaracin (dicha inicializacin
solo se ejecuta la primera vez que se llama a la funcin), as:
static int var1;
void increm()
{
static int a = 5; // Variable local esttica inicializada a 5
a++;
// Incrementar a
}
void main()
{
int i;
// Declarar variable i
// Bucle infinito
}
Variables volatile
A diferencia de los ensambladores, los compiladores tienen cierta
inteligencia. Es decir, piensan un poco antes de traducir el cdigo fuente en
cdigo ejecutable. Por ejemplo, veamos el siguiente pedazo de cdigo para
saber lo que suele pasar con una variable ordinaria:
int var;
//...
var = var;
El ejemplo anterior fue algo burdo, pero habr cdigos con redundancias
aparentes y ms difciles de localizar, cuya optimizacin puede ser
contraproducente. El caso ms notable que destacan los manuales de los
compiladores C para microcontroladores es el de las variables globales que son
accedidas por la funcin de interrupcin y por cualquier otra funcin.
Para que un compilador no intente pasarse de listo con una variable debemos
declararla comovolatile, anteponindole dicho calificador a su declaracin
habitual.
Por ejemplo, en el siguiente boceto de programa la variable count debe ser
accedida desde la funcin interrupt como desde la funcin main; por eso se le
declara como volatile. Nota: el esquema de las funciones de interrupcin suele
variar de un compilador a otro. ste es solo un ejemplo.
volatile int count;
void interrupt(void)
// Funcin de interrupcin
{
// Cdigo que accede a count
}
void main(void)
// Funcin principal
{
// Cdigo que accede a count
}
Arrays y Punteros
Probablemente ste sea el tema que a todos nos ha dado ms de un dolor de
cabeza y que ms hemos reledo para captarlo a cabalidad. Hablo ms bien de
los punteros. Si ellos el C no sera nada, perdera la potencia por la que las
mejores empresas lo eligen para crear sus softwares de computadoras.
Pero bueno, regresando a lo nuestro, estos temas se pueden complicar
muchsimo ms de lo que veremos aqu. Solo veremos los arrays
unidimensionales y los punteros (que en principio pueden apuntar a todo tipo
de cosas) los abocaremos a los datos bsicos, incluyendo los mismos arrays.
Aun as, te sugiero que tengas un par de aspirinas al lado.
Los arrays o matrices
Un array es una mega variable compuesto de un conjunto de variables simples
del mismo tipo y ubicadas en posiciones contiguas de la memoria. Con los
arrays podemos hacer todos lo que hacamos con las tablas (de bsqueda) del
ensamblador y muchsimo ms.
Un array completo tiene un nombre y para acceder a cada uno de sus
elementos se utilizan ndices entre corchetes ([ ]). Los ndices pueden estar
indicados por variables o constantes. En el siguiente esquema se ve que el
primer elemento de un array tiene ndice 0 y el ltimo, N-1, siendo N la
cantidad de elementos del array.
ndice
del
primer
elemento
es 0 y
el
del
ltimo
letters[0] = 'a';
// Aqu el ndice es 0
letters[1] = 'b';
// Aqu el ndice es 1
letters[2] = 'c';
// ...
letters[3] = 'd';
//
letters[4] = 'e';
letters[5] = 'f';
letters[6] = 'g';
letters[7] = 'h';
letters[8] = 'i';
letters[9] = 'j';
// Aqu el ndice es 9
// Ok
texto, donde puede resultar muy incmodo estar contando las letras de una
cadena. Por ejemplo:
int a[] = { 70, 1, 51 };
// Un array de 3 elementos
// Un array de 5 elementos
// Un array de 10 elementos
// Un array de 6 elementos
El array Greet tiene espacio para 10 elementos, de los cuales solo los 5
primeros han sido llenados con las letras de Hello, el resto se rellena con ceros.
El array msg tiene 6 elementos porque adems de las 5 letras de Hello se le
ha aadido unNull (0x00) al final (claro que no se nota). Es decir, la
inicializacin de msg es equivalente a:
char msg[] = { 'H', 'e', 'l', 'l', 'o', 0x00}; // Un array de 6 elementos
Visto grficamente, msg tendra la siguiente representacin:
Los punteros
Apuntando a variables
Decimos que una variable
direccin de dicha variable.
direccin de la variable a
apuntar a una variable cuyo
char a, b, c;
float max;
int * ip;
char * cp;
float * fp;
ip = &height;
ip = &width;
cp = &a;
// cp apunta a a
cp = &c;
// Ahora cp apunta a c
cp = &a;
fp = &max;
// fp apunta a max
fp = &height;
//...
}
Asignaciones indirectas mediante punteros
Una vez que un puntero apunte a una variable cualquiera, se puede acceder a
dicha variable utilizando el nombre del puntero precedido por un asterisco, de
esta forma:
void main (void)
{
int height, width, n; // Variables ordinarias
int * p, * q;
p = &height;
// p apunta a height
*p = 10;
p = &width;
// p apunta a width
*p = 50;
height = *p;
q = &height;
// q apunta a height
n = (*p + *q)/2;
//...
}
La expresin *p se debera leer: la variable apuntada por p. Eso tambin
ayuda mucho a comprender a los punteros.
Y para esto se inventaron los punteros? Yo me preguntaba lo mismo en mis
inicios. El tema de los punteros se puede complicar casi hasta el infinito, por
eso quiero ir con cuidado y poco a poco para que nadie se pierda.
Punteros y arrays
Cmo se declara un puntero a un array? Un puntero a un array es
simplemente un puntero al tipo de dato del array. Cuando se asigna un puntero
a un array, en realidad el puntero toma la direccin de su primer elemento, a
menos que se especifique otro elemento.
Luego, bastara con modificar el valor del puntero para que apunte a los otros
elementos del array. Todo lo indicado se refleja en el siguiente cdigo:
void main (void)
{
int * p;
int n;
// Alguna variable
p = &mat;
n = *p;
// Esto da n = 78
p++;
n = *p;
// Esto da n = 98
p++;
n = *p;
// Esto da n = 26
*p = 10;
p--;
*p = 100;
p = mat;
p = NULL;
// ...
}
En el fondo los arrays y los punteros trabajan de la misma forma, por lo menos
cuando referencian a variables almacenadas en la RAM del microcontrolador.
La nica diferencia es que los arrays no pueden direccionar a datos diferentes
de su contenido; por eso tambin se les llama punteros estticos. En la prctica
esto significa que un array es siempre compatible con un puntero, pero un
puntero no siempre es compatible con un array.
Por ejemplo, a un array no se le puede asignar otro array ni se le pueden sumar
o restar valores para que apunten a otros elementos. Por lo dems, las
operaciones de asignacin son similares para punteros y arrays, tal como se
char a;
a = str1[0];
// Esto da a = 'A'
a = str1[3];
// Esto da a = 'a'
a = str2[0];
// Esto da a = 'P'
a = str2[3];
// Esto da a = 'n'
str1 += 2;
str2 += 2;
str1++;
str2++;
a = *str2;
// Esto da a = 'n'
// ...
}
Paso de punteros y arrays a funciones
Recuerdas el paso de variables por valor y por referencia? Pues aqu vamos de
nuevo.
Bien, recordemos: una variable pasada por valor a una funcin, en realidad le
entrega una copia suya; por lo que la variable original no tiene por qu ser
afectada por el cdigo de la funcin. Ahora bien, pasar una variable por
referencia significa que se pasa la direccin de dicha variable. Como
consecuencia, la funcin tendr acceso a la variable original y podr modificar
su contenido. Esto podra resultar riesgoso, pero, bien usada, la tcnica es una
potente arma.
Ya que los punteros operan con direcciones de variables, son el medio ideal
para trabajar conparmetros por referencia. Hay dos casos de particular
inters: uno, cuando deseamos en serio que la variable pasada a la funcin
cambie a su regreso; y dos, cuando la variable pasada es demasiado grande
(un array) como para trabajar con copias. De hecho, los arrays siempre se
pasan por referencia ya que tambin son punteros al fin.
La sintaxis de los punteros en el encabezado de la funcin no es nada nuevo,
teniendo en cuenta que tambin tienen la forma de declaraciones de variables.
En el siguiente ejemplo la funcin interchange intercambia los valores de las
dos variables recibidas. En seguida explicar por qu vara un poco la forma en
que se llama a la funcin.
void interchange( int * p1, int * p2 )
{
int tmp = *p1; // Guardar valor inicial de variable apuntada por p1.
*p1 = *p2;
}
void main (void)
{
int i, j;
/* Hacer algunas asignaciones */
i = 10;
j = 15;
return ( tmp/size );
// Un array de 4 elementos
while( 1 );
// Bucle infinito
}
Finalmente, veamos un programa donde se utilizan las Cadenas de texto
terminadas en nulo.
Este programa tiene dos funciones auxiliares: mayus convierte la cadena
recibida en maysculas, y lon calcula la longitud del texto almacenado en el
array recibido. Ambas funciones reciben el array pasado en un puntero p dado
que son compatibles.
void mayus( char * p )
{
while( *p ) // Mientras carcter apuntado sea diferente de 0x00
{
if( ( *p >= 'a' ) && ( *p <= 'z' ) ) // Si carcter apuntado es
// minscula
*p = *p - 32;
p++;
}
}
// Hacerlo mayscula
// Incrementar p para apuntar sig. carcter
while( *p )
{
i++;
// Incrementar contador.
p++;
}
return i;
// Retornar i
}
void main (void)
{
int L;
char song1[20] = "Dark Blue";
char song2[20] = "Staring Problem";
char song3[20] = "Ex-Girlfriend";
// Debera dar L = 9
L = lon(song2);
// Debera dar L = 15
L = lon(song3);
// Debera dar L = 13
mayus(song1 );
mayus(song2 );
mayus(song3 );
while(1);
// Bucle infinito
}
En el programa se crean tres arrays de texto de 20 elementos
(song1, song2 y song3), pero el texto almacenado en ellos termina en un
carcter 0x00.
Segn la tabla de caracteres ASCII, las letras maysculas estn ubicadas 32
posiciones por debajo de las minsculas. Por eso basta con sumarle o restarle
ese valor a un carcter ASCII para pasarlo a mayscula o minscula.
En ambas funciones el puntero p navega por los elementos del array apuntado
hasta que encuentra el final, indicado por un carcter nulo (0x00).
Arrays constantes
No es que me haya atrasado con el tema, es solo que los arrays constantes son
uno de los temas cuyo tratamiento vara mucho entre los distintos
compiladores. Veamos en qu.
Un array constante es uno cuyos elementos solo podrn ser ledos pero no
escritos; tan simple como eso.
En principio, para que un array sea constante a su clsica declaracin
con inicializacin de un array se le debe anteponer el calificador const. No es
posible declarar un array constante vaco y llenar sus elementos despus pues
eso equivaldra a modificar sus elementos. Enseguida tenemos ejemplos de
declaracin de arrays constantes:
const int a[5] = { 20, 56, 87, -58, 5000 };
// Array constante
const char vocals[5] = { 'a', 'e', 'i', 'o', 'u' }; // Array constante
vocals[0] = 'a'; // Error! no se puede escribir por mas que sea el mismo dato
Ahora bien, que los datos no cambien durante la ejecucin del programa no
necesariamente significa los arrays constantes estn ubicados en la
memoria FLASH. En algunos compiladores de PICs, como CCS C y Hitech C, s
ocurre as, pero el lenguaje C solo dice que estos datos son inmodificables, no
dice dnde deben residir. Recordemos que las variables en un programa de
computadora, constantes o no, van siempre en la RAM. Para las computadoras
no es problema porque les "sobra" la RAM, cosa que no sucede en los
microcontroladores.
Variables PROGMEM y su acceso
Por otro lado, los compiladores de AVR ofrecen mtodos alternos para declarar
arrays, o variables en general, que puedan residir en la memoria FLASH. Con
eso no solo se destina la preciada RAM a otras variables sino que se pueden
usar grandes arrays constantes que simplemente no podran caber en la RAM.
Como todo esto va al margen de lo que diga el ANSI C, cada compilador ha
establecido su propia sintaxis para hacerlo.
Empecemos por examinar el estilo de AVR GCC. Por ejemplo, si queremos que
los tres primeros arrays de esta pgina se almacenen en la FLASH debemos
declararlas e inicializarlas de esta forma
PROGMEM const int a[5] = { 20, 56, 87, -58, 5000 };
// Array constante
PROGMEM const char vocals[5] = { 'a', 'e', 'i', 'o', 'u' }; // Array constante
PROGMEM const char text[] = "Este es un array constante de caracteres";
Observa que fue tan simple como aadirles al inicio la palabra
reservada PROGMEM. El calificadorconst era opcional en las versiones pasadas
de AVR GCC como la que viene con AVR Studio 5, pero es necesaria en las
versiones recientes como la que trae Atmel Studio 6. De todos modos es
sencillo. Lo complicado viene despus. Para acceder a los elementos de estos
arrays hay que emplear una forma un tanto extica. Se deben usar algunas
macros propias del compilador, todas provedas por la librera pgmspace.h. Las
principales son estas cuatro
// Array constante
PROGMEM const char vocals[5] = { 'a', 'e', 'i', 'o', 'u' }; // Array constante
PROGMEM const char text[] = "Este es un array constante de caracteres";
int var;
char c;
var = pgm_read_word(&a[1]);
El manual de AVR GCC nos presenta una forma que puede resultar ms fcil de
asimilar el acceso a los elementos de estos arrays: dice que primero asumamos
acceder al elemento como si perteneciera a un array ordinario (residente en
RAM), por ejemplo:
var = a[1];
luego le aplicamos el operador &
var = &a[1];
/*****************************************************************************
* Toca las notas del ringtone apuntado por pRingtone.
****************************************************************************/
void Tune(PGM_P pRingtone)
{
// C C# D D# E F F#
PROGMEM
const
{262,277,294,311,330,349,370};
unsigned
int
NoteFreqs[]
= {0,812,406,270,203,162,135};
/* ... */
}
Sucede que los arrays estn declarados como si fueran locales ordinarias. Si los
hubiramos declarado globalmente estara bien. Pero como son locales es
necesario que sean adems de tipo static. Como sabemos, estas variables en C
se forman aadindoles la palabra reservadastatic a su declaracin habitual.
Con esto aclarado, el cdigo anterior trabajar perfectamente si lo escribimos
de esta forma.
/*****************************************************************************
* Toca las notas del ringtone apuntado por pRingtone.
****************************************************************************/
void Tune(PGM_P pRingtone)
{
// C C# D D# E F F#
static
PROGMEM
{262,277,294,311,330,349,370};
const
unsigned
int
NoteFreqs[]
unsigned
int
Bpms[]
/* ... */
}
Ese trozo de cdigo pertenece al programa de la prctica reproductor de
ringtones PICAXE. Si deseas comprobar lo expuesto puedes descargarlo y
recompilarlo haciendo las modificaciones explicadas.
Los punteros PGM_P y PGM_VOID_P
Justo en el ejemplo anterior aparece el tipo de dato PGM_P en una de sus
funciones que es permitir el paso de variables a funciones. Ese tema lo
profundizaremos luego.
El tipo de dato PGM_P es un puntero a una variable residente en la memoria
FLASH. Su definicin en el archivo pgmspace.h de AVR GCC es
#define PGM_P const PROGMEM char *
pero el archivo pgmspace.h de AVR IAR C lo define como
#define PGM_P const char __flash *
Con el fin de que los programas de cursomicros sean lo ms transparentes
posible trato de evitar el uso excesivo de los #defines que conducen a
trminos innecesarios. El hecho de estar estudiando PGM_P sugiere que se
trata
de
una
excepcin.
Notemos
en
primer
lugar
que
el
archivo avr_compiler.h que
se
usa
en
esta
web
define PROGMEM como __flash con lo cual las dos expresiones de arriba seran
idnticas asumiendo que en AVR GCC const PROGMEM charequivale a const
char PROGMEM y tambin a PROGMEM const char, siendo esta ltima
presentacin la forma en que hemos venido trabajando. Debido a ello en
muchas ocasiones podremos prescindir de PGM_P, pero surgirn algunos casos
en que AVR IAR C muestre su disconformidad por ese reacomodo de
trminos. PGM_P no solo termina de arreglar estos desajustes sino que facilita
notablemente la escritura del cdigo ante la aparicin de construcciones ms
complejas como las que veremos despus.
Si PGM_P define un tipo puntero que apunta a variables char (o de un byte en
general), alguien podra preguntar cules son los punteros para las variables de
tipo int, short, float, etc. No hay definiciones especiales para esos casos.
Podemos crearlas por cuenta propia si deseamos pero ser raramente
PROGMEM const unsigned int Notes[] = {262, 277, 294, 311, 494};
PROGMEM const unsigned char Octaves[] = {6, 7, 5};
int main(void)
{
PGM_P p;
// Declarar el puntero p
unsigned char c;
unsigned int n;
p = (PGM_P)Octaves;
c = pgm_read_byte(p + 1);
p = (PGM_P)Notes;
while(1);
}
Como los arrays pueden ser entendidos como punteros tambin, en principio
se podran hacer las asignaciones a p directamente como p = Notes, pero para
evitar protestas del compilador se deben usar conversiones de tipo, poniendo
PROGMEM const unsigned int Notes[] = {262, 277, 294, 311, 494};
PROGMEM const unsigned char Octaves[] = {6, 7, 5};
int main(void)
{
PGM_VOID_P p;
// Declarar el puntero p
unsigned char c;
unsigned int n;
p = (PGM_VOID_P)Octaves;
p = (PGM_VOID_P)Notes;
while(1);
}
Los punteros PGM_P y PGM_VOID_P tambin pueden actuar sobre variables de
tipo complejo. Estamos hablando por ejemplo de estructuras definidas por el
usuario. Por el mismo hecho de ser variables complejas poner aqu un
programa de demostracin abarcara demasiado espacio. Prefiero remitirme a
la librera para USB que distribuye Atmel. Me parece un perfecto ejemplo.
Puedes
encontrarla
en
varias
notas
de
aplicacin
como AVR270, AVR271, AVR272 y AVR273,
por
citar
algunas.
En
el
archivo usb_standar_request.c se declara y usa el puntero pbuffer de
tipoPGM_VOID_P para acceder a los descriptores de USB que por su tamao
residen en la FLASH.
Esa librera USB se vale de un archivo llamado compiler.h para guardar la
compatibilidad de cdigos entre los compiladores AVR IAR C y AVR GCC para
los que est escrita. Contiene varias imprecisiones que, imagino, se deben a
los defectos que AVR GCC presentaba antiguamente, cuando se escribi la
librera. Igual vale la pena revisarla.
/*****************************************************************************
* Enva por el USART el texto pasado en p
****************************************************************************/
void print(PGM_P p)
{
char c;
while( (c = pgm_read_byte(p++)) != 0x00 )
putchar(c);
}
/******************************************************************************
* Main function
*****************************************************************************/
int main(void)
{
usart_init();
// Inicializar USART
print(rt01);
print(rt02);
// ...
print(rt03);
// ...
print(rt04);
// ...
while (1);
}
Creo que el cdigo est bastante claro. Como los arrays son de texto (de
caracteres de 1 byte), se opt por el puntero PGM_P y por la
macro pgm_read_byte para la que no fue necesaria una conversin de tipo. La
conversin de tipo para p es opcional, por ejemplo, tambin se pudo
escribir print((PGM_P)rt01).
Y ahora la pregunta que nos trae aqu: Se puede enviar a una funcin una
variable en flash directamente? Es decir, qu pasa si en vez de declarar los
arrays por separado, los escribimos directamente en el argumento de la
siguiente forma.
print("\r Deck the halls");
print("\r Jingle bells");
// ...
// ...
// ...
PGM_P ringtones[] =
{
ringt01,
ringt02,
ringt03,
ringt04
};
/******************************************************************************
* Main function
*****************************************************************************/
int main(void)
{
usart_init();
// Inicializar USART
puts_P(ringtones[0]);
puts_P(ringtones[1]);
puts_P(ringtones[2]);
puts_P(ringtones[3]);
while (1);
}
La funcin puts_P es proveda por los compiladores. Es similar a la
funcin puts pero las cadenas que recibe deben ubicarse en la FLASH. En otras
palabras, puts_P es similar a la funcin printque creamos en el ejemplo previo.
Se nota que el array ringtones ha sido declarado para ubicarse en la RAM, por
eso accedemos a sus elementos de forma regular y no empleando macros. Se
hizo as porque cada elemento es un puntero de 2 bytes y como solo son 4
punteros, no ocupan mucho espacio. Si hubiera muchos ms elementos en el
array ringtones, la situacin cambiara y sera mejor que tambin residiera en
la FLASH. Eso lo veremos al final.
Elaborar el array y el contenido de sus elementos por separado es incmodo
por el hecho de tener que poner nombres a cada elemento, nombres que no
son necesarios en otra parte del programa, pero no hay otro camino.
Quisiramos que fuera posible implementar el array por ejemplo como se
muestra abajo donde cada elemento se inicializa directamente, pero eso solo
/******************************************************************************
* Main function
*****************************************************************************/
int main(void)
{
usart_init();
// Inicializar USART
while (1);
}
La conversin de tipo con (PGM_P) no es necesaria para AVR IAR C y para AVR
GCC sirve para evitar warnings aunque el programa funciona igual.
// Array constante
PROGMEM const char vocals[5] = { 'a', 'e', 'i', 'o', 'u' }; // Array constante
PROGMEM const char text[] = "Este es un array constante de caracteres";
int var;
char c;
var = pgm_read_word_far(&a[1]);
Podemos entender que los nicos AVR que aceptan estas macros son los que
tienen ms de 64 KB de memoria FLASH. Con esos AVR, es posible usar los dos
tipos de macros, las de 16 bits y las de 32 bits, sin embargo no siempre sern
igual de eficientes. Si el cdigo de arriba, por ejemplo, estuviera escrito para
un ATmega1284P, el acceso se dilatara ligeramente al tenerse que trabajar
con 32 bits. Puede ser un detalle insignificante pero a veces servir para
optimizar procesos.
Para complementar el tema, diremos que si existen macros con _far (lejos, en
ingls),
en
la
librera pgmspace.h tambin
hay
macros
con
el
apndice _near (cerca).
Estas
nuevas
macros
sonpgm_read_byte_near, pgm_read_word_near, pgm_read_dword_near y pgm_r
ead_float_near. Pero no te preocupes si crees que el tema se va recargando
demasiado. No se tratan ms que de alias de las primeras macros de 16 bits
que estudiamos arriba, por ejemplo,pgm_read_byte_near es lo mismo
que pgm_read_byte, y as con las dems. Es bueno saberlo para no quedar
sorprendidos por los cdigos de quienes prefieren usar la una u otra forma.