Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Programacion C AVR
Programacion C AVR
-----------------------------------------------------------------------------------------------------------------------------
CURSO: MICROCONTROLADORES
CALLAO, 2014V
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 1
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
INTRODUCCIÓN
Los lenguajes de alto nivel son mucho más potentes que el ensamblador aunque su aprendizaje
demanda un mayor esfuerzo.
Para empezar a programar en ensamblador nos puede bastar con aprender unas 50 palabras (las
instrucciones básicas).
ESTRUCTURA DE UN PROGRAMA EN C
Tomaremos en cuenta este sencillísimo ejemplo, escrito para los compiladores AVR IAR C y
AVR GCC.
/************************************************************************
* FileName: main.c
* Purpose: LED parpadeantwe
* Processor: ATmel AVR
* Compiler: AVR IAR C & AVR GCC (WinAVR)
* Author:
*************************************************************************/
#include "avr_compiler.h"
//****************************************************************************
// delay_ms
//****************************************************************************
void delay_ms(unsigned int t)
{
while(t--)
delay_us(1000);
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 2
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
}
//****************************************************************************
// Función principal
//****************************************************************************
int main(void)
{
DDRB = 0x01; // Configurar pin PB0 como salida
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
cíclicamente.
1.-LOS COMENTARIOS
Ejemplos.
/*
Ésta es una forma de comentar varias líneas a la vez.
Sirve mucho para enmascarar bloques de código.
*/
Una sentencia es algo así como una mega instrucción, que hace lo que varias instrucciones del
ensamblador.
Salvo casos particulares, donde su uso es opcional, una sentencia debe finalizar con un punto
y coma (;).
Así que también podemos entender que los; sirven para separar las sentencias. Alguna vez leí
que el compilador C lee el código como si lo absorbiera con una cañita, línea por línea, una a
continuación de otra (evadiendo los comentarios por supuesto).
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 3
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
Por ejemplo, la función main del programa de arriba bien puede escribirse del siguiente modo.
//****************************************************************************
// Función principal
//****************************************************************************
int main(void)
{
DDRB = 0x01;
for( ;; )
{
PORTB |= 0x01;
delay_ms(400);
PORTB &= 0xFE;
delay_ms(300);
}
}
¿Sorprendido? Podrás deducir que los espacios y las tabulaciones solo sirven para
darle un aspecto ordenado al código. Es una buena práctica de programación
aprender a acomodarlas.
Un bloque establece y delimita el cuerpo de las funciones y algunas sentencias mediante llaves
({}).
Como ves en el ejemplo de arriba, las funciones main y pausa tienen sus bloques, así como los
bucles while y for. Creo que exageré con los comentarios, pero sirven para mostrarnos dónde
empieza y termina cada bloque. Podrás ver cómo las tabulaciones ayudan a distinguir unos
bloques de otros. Afortunadamente, los editores de los buenos compiladores C pueden resaltar
cuáles son las llaves de inicio y de cierre de cada bloque. Te será fácil acostumbrarte a usarlas.
4.-LAS DIRECTIVAS
Entre las pocas directivas del C estándar que también son soportadas por los compiladores C
para AVR están;
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 4
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
En un programa en C puede haber las funciones que sean posibles, pero nunca debe faltar la
función principal, llamada main.
Donde quiera que se encuentre, la función main siempre será la primera en ser ejecutada. De
hecho, allí empieza y no debería salir de ella.
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 números de 8, 16 ó 32 bits (a veces más
grandes), con signo o sin él, números enteros o decimales. Esos son los tipos de datos básicos.
Las variables de los compiladores pueden incluso almacenar matrices de datos del mismo tipo
(llamadas arrays) o de tipos diferentes (llamadas estructuras). Estos son los tipos de datos
complejos.
Los siguientes son los principales tipos de datos básicos del lenguaje C. Observa que la tabla los
separa en dos grupos, los tipos enteros y los tipos de punto flotante.
Afortunadamente, a diferencia de los compiladores para PIC, los compiladores para AVR suelen
respetar bastante los tipos establecidos por el ANSI C. Algunos compiladores también manejan
tipos de un bit como bool (o boolean) o bit, pero con pequeñas divergencias que pueden afectar
la portabilidad de los códigos además de confundir a los programadores. Esos tipos son
raramente usados.
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 5
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
Por defecto el tipo double es de 32 bits en los microcontroladores. En ese caso es equivalente al
tipo float. Los compiladores más potentes como AVR IAR C y AVR GCC sin embargo ofrecen
la posibilidad de configurarlo para que sea de 64 bits y poder trabajar con datos más grandes y
de mayor precisión.
Los especificadores signed (con signo) mostrados entre paréntesis son opcionales. Es decir, da
lo mismo poner int que signed int, por ejemplo. Es una redundancia que se suele usar para
“reforzar” su condición o para que se vea más ilustrativo.
El tipo char está pensado para almacenar caracteres ASCII como las letras. Puesto que estos
datos son a fin de cuentas números también, es común usar este tipo para almacenar números de
8 bits. Es decir es equivalente a signed char o unsigned char, dependiendo de la configuración
establecida por el entorno compilador. Y como es preferible dejar de lado estas cuestiones, si
vamos a trabajar con números lo mejor es poner el especificador signed o unsigned en el código.
Quizá te preguntes cuál es la diferencia entre los tipos de datos int y short si aparentemente
tienen el mismo tamaño y aceptan el mismo rango de valores. Esa apariencia es real en el
entorno de los microcontroladores AVR. Es decir, al compilador le da lo mismo si ponemos int
o short. Sucede que el tipo short fue y siempre debería ser de 16 bits, en tanto que int fue
concebido para adaptarse al bus de datos del procesador. Esto todavía se cumple en la
programación de las computadoras, por ejemplo, un dato int es de 32 bits en un Pentium IV y es
de 64 bits en un procesador Core i7. De acuerdo con este diseño un tipo int debería ser de 8 bits
en un megaAVR y de 32 bits en un AVR32. Sin embargo, la costumbre de relacionar el tipo int
con los 16 bits de las primeras computadoras como las legendarias 286 se ha convertido en
tradición y en regla de facto para los microcontroladores. Actualmente solo en CCS C el tipo int
es de 8 bits. Es irónico para ser el compilador que menos respeta los tipos de datos del ANSI C.
A pesar de todo, se nota que todavía pueden aparecer ciertas imprecisiones en los tipos de datos
que pueden perturbar la portabilidad de los programas entre los diferentes compiladores. Es por
esto que el lenguaje C/C++ provee la librería stdint.h para definir tipos enteros que serán de un
tamaño específico independientemente de los procesadores y de la plataforma software en que
se trabaje.
Es fácil descubrir la estructura de estos tipos para familiarizarse con su uso. Para ello debemos
en primer lugar incluir en nuestro programa el archivo stdint.h con la siguiente directiva.
#include <stdint.h>
Esta inclusión ya está hecha en el archivo avr_compiler.h que se usa en todos los programas de
curso, así que no es necesario volverlo a hacer. Aunque el objetivo de este archivo es permitir la
compatibilidad de códigos entre los compiladores AVR IAR C y AVR GCC, debemos saber que
en AVR IAR C el archivo avr_compiler.h solo está disponible al usar la librería DLIB. Como
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 6
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
las prácticas de curso trabajan sobre la librería CLIB, he evitado recurrir a los tipos extendidos
de stdint.h.
Finalmente, existen además de los vistos arriba otros tipos y especificadores de datos que no son
parte del lenguaje C pero que fueron introducidos por los compiladores pensando en las
características especiales de los microcontroladores. Muchos de ellos son redundantes o simples
alias y algunos que sí son de utilidad como el tipo PGM_P los veremos en su momento.
7.-Declaración de variables
Esta parte es comparable, aunque lejanamente a cuando se identifican las variables del
ensamblador con la directiva .def. No se puede usar una variable si antes no se ha declarado. La
forma general más simple de hacerlo es la siguiente:
data_type myvar;
Donde data_type es un tipo de dato básico o complejo, del compilador o definido por el usuario
y myvar es un identificador cualquiera, siempre que no sea palabra reservada.
Ejemplos.
unsigned char d; // Variable para enteros de 8 bits sin signo
char b; // Variable de 8 bits (para almacenar
// caracteres ascii)
signed char c; // Variable para enteros de 8 bits con signo
int i; // i es una variable int, con signo
signed int j; // j también es una variable int con signo
unsigned int k; // k es una variable int sin signo
También es posible declarar varias variables del mismo tipo, separándolas con comas. Así nos
ahorramos algo de tipeo. Por ejemplo:
Una variable const debe ser inicializada en su declaración. Después de eso el compilador solo
permitirá su lectura mas no su escritura. Ejemplos:
//...
b = a; // Válido
b = 150; // Válido
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 7
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
Por más que las variables constantes sean de solo lectura, ocuparán 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 clásicas
directivas #define (como se hace en el ensamblador).
9.-SENTENCIAS SELECTIVAS
Llamadas también sentencias de bifurcación, sirven para redirigir el flujo de un programa según
la evaluación de alguna condición lógica.
Las sentencias if e if–else son casi estándar en todos los lenguajes de programación. Además de
ellas están las sentencias if–else escalonadas y switch–case.
9.1 La sentencia if
La sentencia if (si condicional, en inglés) hace que un programa ejecute una sentencia o un
grupo de ellas si una expresión es cierta. Esta lógica se describe en el siguiente esquema.
sentenciaA;
if ( expression ) // Si expression es verdadera,
// ejecutar el siguiente bloque
{ // apertura de bloque
sentenciaB;
sentenciaC;
// algunas otras sentencias
} // cierre de bloque
sentenciaX;
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 8
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
Expresando lo descrito en código C, tenemos: (Se lee como indican los comentarios.)
SentenciaA;
if ( expression ) // Si expression es verdadera, ejecutar
{ // este bloque
sentenciaB;
sentenciaC;
// ...
}
else // En caso contrario, ejecutar este bloque
{
sentenciaM;
sentenciaN;
// ...
}
sentenciaX;
// ...
Como ves, es bastante fácil, dependiendo del resultado se ejecutará uno de los dos bloques de la
sentencia if – else, pero nunca los dos a la vez.
En el siguiente boceto se comprueban tres condiciones lógicas, aunque podría haber más. Del
mismo modo, se han puesto dos sentencias por bloque solo para simplificar el esquema.
Las “expresiones” se evalúan de arriba abajo. Cuando alguna de ellas sea verdadera, se ejecutará
su bloque correspondiente y los demás bloques serán salteados. El bloque final (de else) se
ejecuta si ninguna de las expresiones es verdadera. Además, si dicho bloque está vacío, puede
ser omitido junto con su else.
Para elaborar el código en C se usan las palabras reservadas switch, case, break y default.
El siguiente esquema presenta tres case’s pero podría haber más, así como cada bloque también
podría tener más 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: //Si expression no fue igual a ninguna de las
// constantes anteriores, ejecutar este bloque
sentencia7;
sentencia8;
break;
}
sentenciaX;
// todo...
donde constante1, constante2 y constante3 deben ser constantes enteras, por ejemplo, 2, 0x45,
‘a’, etc. (‘a’ tiene código ASCII 165, que es, a fin de cuentas, un entero.)
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 10
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
Expresión puede ser una variable compatible con entero. No es una expresión que conduce a
una condición lógica como en los casos anteriores.
El programa solo ejecutará uno de los bloques dependiendo de qué constante coincida con
expression. Usualmente los bloques van limitados por llaves, pero en este caso son opcionales,
dado que se pueden distinguir fácilmente. 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, también se ejecutará el bloque del
siguiente case, sin importar si su constante coincida con expression o no.
Las sentencias de control iterativas sirven para que el programa ejecute una sentencia o un
grupo de ellas un número determinado o indeterminado de veces. Así es, esta sección 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 versión más compacta
e intuitiva del bucle while.
El bucle while en C tiene la siguiente sintaxis y se lee así: mientras (while) expression sea
verdadera, ejecutar el siguiente bloque.
sentenciaA;
while ( expression ) // Mientras expression sea verdadera, ejecutar el
// siguiente bloque
{
sentenciaB;
sentenciaC;
// ...
}; // Este ; es opcional
sentenciaX;
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 11
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
// ...
Nota que en este caso primero se evalúa expression. Por lo tanto, si desde el principio
expression es falsa, el bloque de while no se ejecutará nunca. Por otro lado, si expression no
deja de ser verdadera, el programa se quedará dando vueltas “para siempre”.
sentenciaA;
do
{
sentenciaB;
sentenciaC;
// ...
} while ( expression ); // Este ; es mandatorio
sentenciaX;
// ...
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 12
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
Por la forma y orden en que se ejecutan estas expresiones, el bucle for es equivalente a la
siguiente construcción, utilizando la sentencia while. Primero se ejecuta expression_1 y luego se
ejecuta el bloque indicado tantas veces mientras expression_2 sea verdadera.
expression_1;
while ( expression_2 )
{
sentencia1;
sentencia2;
// ...
expression_3;
}
No obstante, de esa forma se ve más rara aún; así que, mejor, veamos estos ejemplos, que son
sus presentaciones más clásicas. (i es una variable y a y b son constantes o variables):
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 que
i 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.
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.
Se pueden hacer muchas más construcciones, todas coincidentes con la primera plantilla, pero
también son menos frecuentes.
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.
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 13
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
if(a > b)
{
a = 0;
}
if(a == b)
{
a++;
}
else
{
b--;
}
while( a >= b)
{
a = a + b;
}
if(a > b)
a = 0;
if(a == b)
a++;
else
b--;
while( a >= b)
a = a + b;
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 14
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
Ejemplos:
¿Te recordaron a tus clases de álgebra del colegio? A diferencia de esas matemáticas, estas
expresiones no son ecuaciones; significan las operaciones que indican sus comentarios.
Por lo visto, los operadores ++ y -- funcionan igual si están antes o después de una variable en
una expresión simple. Sin embargo, hay una forma (tal vez innecesaria y confusa para un
novato, pero muy atractiva para los que ya estamos acostumbrados a su uso) que permite
escribir código más compacto, es decir, escribir dos sentencias en una.
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 15
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
Si bien son operaciones que producen resultados análogos a los de las instrucciones de
ensamblador los operadores lógicos del C pueden operar sobre variables de distintos tamaños,
ya sean de 1, 8, 16 ó 32 bits.
Ejemplos:
Fíjate en la semejanza entre las operaciones de desplazamiento con >> y << y las operaciones
del rotación del ensamblador. Cuando una variable se desplaza hacia un lado, los bits que salen
por allí se pierden y los bits que entran por el otro lado son siempre ceros. Es por esto que en la
última sentencia, m = m << 8, el resultado es 0x00. Por cierto, en el lenguaje C no existen
operadores de rotación. Hay formas alternativas de realizarlas.
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 16
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
Se emplean para construir las condiciones lógicas 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.
Generalmente se utilizan para enlazar dos o más condiciones lógicas simples. Por suerte, estos
operadores solo son tres y serán explicados en las prácticas del curso.
Ejemplos:
Se utiliza en las operaciones de asignación y nos permite escribir código más abreviado. La
forma general de escribir una sentencia de asignación mediante los operadores compuestos es:
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 17
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
op puede ser cualquiera de los operadores aritméticos o de bit estudiados arriba. O sea, op puede
ser +, - , *, /, %, &, | , ^, ~, << ó >>. Nota: no debe haber ningún espacio entre el operador y el
signo igual.
Ejemplos:
int a; // Declarar a
a += 50; // Es lo mismo que a = a + 50;
a += 20; // También significa sumarle 20 a a
a *= 2; // Es lo mismo que a = a * 2;
a &= 0xF0; // Es lo mismo que a = a & 0xF0;
a <<= 1; // Es lo mismo que a = a << 1;
b = a * b + c / b; // a, b y c son variables
A diferencia del lenguaje Basic, donde la expresión se evalúa de izquierda a derecha, en esta
sentencia no queda claro en qué orden se ejecutarán las operaciones indicadas. Hay ciertas
reglas que establecen dichas prioridades; por ejemplo, las multiplicaciones y divisiones siempre
se ejecutan antes que las sumas y restas. Pero es más práctico emplear los paréntesis, los cuales
ordenan que primero se ejecuten las operaciones de los paréntesis más internos. Eso es como en
el álgebra elemental de la escuela, así que no profundizaré.
b = (a * b) + (c / b);
b = a * (b + (c / b));
b = ((a * b) + c)/ b);
Una función 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), después se
ejecuta todo el código de la función y finalmente se recobra el PC para regresar de la función.
Dada su relativa complejidad, no es tan simple armar una plantilla general que represente a
todas las funciones. El siguiente esquema es una buena aproximación.
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 18
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
Donde:
function_name();
La función principal main es otro ejemplo de función sin parámetros. Dondequiera que se
ubique, siempre debería ser la primera en ejecutarse; de hecho, no debería terminar.
Por el momento, solo estudiaremos las funciones que pueden tener varios parámetros de entrada
pero solo uno de salida.
Para llamar a una función con parámetros es importante respetar el orden y el tipo de los
parámetros que ella recibe. El primer valor pasado corresponde al primer parámetro de entrada;
el segundo valor, al segundo parámetro; y así sucesivamente si hubiera más.
Cuando una variable es entregada a una función, en realidad se le entrega una copia suya. De
este modo, el valor de la variable original no será alterado. Mejor, plasmemos todo esto en el
siguiente ejemplo.
En el programa mostrado la función minor recibe tres parámetros de tipo int y devuelve uno,
también de tipo int, que será el menor de los números recibidos.
Aunque el C no es tan implacable con la comprobación de tipos de datos como Pascal, siempre
deberíamos revisar que los datos pasados sean compatibles con los que la función espera, así
como los datos recibidos, con los que la función devuelve. Por ejemplo, estaría mal llamar a la
función minor del siguiente modo:
Aquí los dos primeros parámetros están bien, pero el tercero es un número decimal (de 32 bits),
no compatible con el tercer parámetro que la función espera (entero de 16 bits). En estos casos
el compilador nos mostrará mensajes de error, o cuando menos de advertencia.
La función que recibe un parámetro por referencia puede cambiar el valor de la variable pasada.
La forma clásica de estos parámetros se puede identificar por el uso del símbolo &, tal como se
ve en el siguiente boceto de función.
int minor ( int & arg1, int & arg2, int & arg3 )
{
// Cuerpo de la función.
// arg1, arg2 y arg3 son parámetros por referencia.
// Cualquier cambio hecho a ellos desde aquí afectará a las
variables
// que fueron entregadas a esta función al ser llamada.
}
No voy profundizar al respecto porque he visto que muchos compiladores C no soportan esta
forma. Otra forma de pasar un parámetro por referencia es mediante los punteros, pero eso lo
dejamos para el final porque no es nada nada fácil para un novato.
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 20
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
El prototipo de una función le informa al compilador las características que tiene, como su tipo
de retorno, el número de parámetros que espera recibir, el tipo y orden de dichos parámetros.
Por eso se deben declarar al inicio del programa.
El prototipo de una función 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 y
func2 declaradas al inicio del archivo permitirán que dichas funciones sean accedidas desde
cualquier parte del programa. Además, sin importar dónde se ubique la función main, ella
siempre será la primera en ejecutarse. Por eso su prototipo de función es opcional.
#include <avr.h>
void main(void)
{
// Cuerpo de la función
// Desde aquí se puede acceder a func1 y func2
}
void func1(char m, long p)
{
// Cuerpo de la función
// Desde aquí se puede acceder a func2 y main
}
char func2(int a)
{
// Cuerpo de la función
// Desde aquí se puede acceder a func1 y main
}
La llamada a main, por supuesto, no tiene sentido; solo lo pongo para ilustrar.
Si las funciones no tienen prototipos, el acceso a ellas será restringido. El compilador solo verá
las funciones que están implementadas encima de la función llamadora o, de lo contrario,
mostrará errores de “función no definida”. El siguiente boceto ilustra este hecho. (Atiende a los
comentarios.)
#include <avr.h>
void main(void)
{
// Cuerpo de la función
// Desde aquí no se puede acceder a func1 ni func2 porque están abajo
}
void func1(char m, long p)
{
// Cuerpo de la función
// Desde aquí se puede acceder a main pero no a func2
}
char func2(int a)
{
// Cuerpo de la función
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 21
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
Para terminar, dado que los nombres de las variables en los parámetros de entrada son
opcionales, los prototipos de func1 y func2 también se pueden escribir asi
Los lenguajes de alto nivel como el C fueron diseñados para desarrollar los programas más
grandes y complejos que se puedan imaginar, programas donde puede haber cientos de
variables, entre otras cosas. ¿Imaginas lo que significaría buscar nombres para cada variable si
todos tuvieran que ser diferentes? Pues bien, para simplificar las cosas, el C permite tener varias
variables con el mismo nombre.
Así es. Esto es posible gracias a que cada variable tiene un ámbito, un área desde donde será
accesible. Hay diversos tipos de ámbito, pero empezaremos por familiarizarnos con los dos más
usados, que corresponden a las variables globales y variables locales.
Las variables declaradas fuera de todas las funciones y antes de sus implementaciones
tienen carácter global y podrán ser accedidas desde todas las funciones.
Las variables declaradas dentro de una función, incluyendo las variables del
encabezado, tienen ámbito local. Ellas solo podrán ser accedidas desde el cuerpo de
dicha función.
De este modo, puede haber dos o más variables con el mismo nombre, siempre y cuando estén
en diferentes funciones. Cada variable pertenece a su función y no tiene nada que ver con las
variables de otra función, por más que tengan el mismo nombre.
Por ejemplo, en el siguiente boceto de programa hay dos variables globales (speed y limit) y
cuatro variables locales, tres de las cuales se llaman count. Atiende a los comentarios.
void inter(void)
{
int count; // Variable local
/* Este count no tiene nada que ver con el count
de las funciones main o foo */
Algo muy importante: a diferencia de las variables globales, las variables locales tienen
almacenamiento temporal, es decir, se crean al ejecutarse la función y se destruyen al salir de
ella. ¿Qué significa eso? Lo explico en el siguiente apartado.
Si dentro de una función hay una variable local con el mismo nombre que una variable global, la
precedencia en dicha función la tiene la variable local. Si te confunde, no uses variables
globales y locales con el mismo nombre.
Cuando se llama a una función sus variables locales se crearán en ese momento y cuando se
salga de la función se destruirán. Se entiende por destruir al hecho de que la locación de
memoria que tenía 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 función.
Por ejemplo, revisa la siguiente función, donde a es una variable local ordinaria.
void increm()
{
int a; // Declarar variable a
a++; // Incrementar a
}
Cualquiera que haya sido su valor inicial, ¿crees que después de llamar a esta función 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 función, se destruye.
Para que una variable tenga una locación de memoria independiente y su valor no cambie entre
llamadas de función tenemos dos caminos: o la declaramos como global, o la declaramos como
local estática. Los buenos programadores siempre eligen el segundo.
Una variable se hace estática anteponiendo a su declaración el especificador static. Por defecto
las variables estáticas se auto inicializan a 0, pero se le puede dar otro valor en la misma
declaración (dicha inicialización solo se ejecuta la primera vez que se llama a la función), así:
Ejemplos.
void increm()
{
static int a = 5; //Variable local estática inicializada a 5
a++; // Incrementar a
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 23
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
}
void main()
{
int i; // Declarar variable i
El compilador creerá (probablemente como nosotros) que la sentencia var = var no tiene sentido
(y quizá tenga razón) y no la tendrá en cuenta, la ignorará. Ésta es solo una muestra de lo que
significa optimización del código. Luego descubrirás más formas de ese trabajo.
El ejemplo anterior fue algo burdo, pero habrá códigos con redundancias aparentes y más
difíciles de localizar, cuya optimización puede ser contraproducente. El caso más notable que
destacan los manuales de los compiladores C para microcontroladores es el de las variables
globales que son accedidas por la función de interrupción y por cualquier otra función.
Para que un compilador no intente “pasarse de listo” con una variable debemos declararla como
volatile, anteponiéndole dicho calificador a su declaración habitual.
Por ejemplo, en el siguiente boceto de programa la variable count debe ser accedida desde la
función interrupt como desde la función main; por eso se le declara como volatile. Nota: el
esquema de las funciones de interrupción suele variar de un compilador a otro. Éste es solo un
ejemplo.
Probablemente éste sea el tema que a todos nos ha dado más de un dolor de cabeza y que más
hemos releído para captarlo a cabalidad. Hablo más bien de los punteros. Si ellos el C no sería
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 24
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
nada, perdería 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 muchísimo más 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 básicos, incluyendo los mismos
arrays. Aun así, te sugiero que tengas un par de aspirinas al lado.
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
hacíamos con las tablas (de búsqueda) del ensamblador y muchísimo más.
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.
Para el array letters el primer elemento es letters[0] y el último, letters[9]. Así, tenemos 10
elementos en total. Si quisiéramos asignar a cada uno de los elementos de letters los caracteres
desde la ‘a’ hasta la ‘j’, lo podríamos hacer individualmente así:
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 25
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
Pero así no tiene gracia utilizar arrays. En este caso lo mejor es utilizar un bucle, así: (Nota: los
caracteres son, al fin y al cabo, números en códigos ASCII y se les puede comparar.)
char c;
for ( c = 'a'; c <= 'j'; c++ )
letters[i] = c;
Ejemplos:
También es posible inicializar un array sin especificar en su declaración el tamaño que tendrá,
dejando los corchetes vacíos. El tamaño será pre calculado y puesto por el compilador. Ésta es
una forma bastante usada en los arrays de texto, donde puede resultar muy incómodo estar
contando las letras de una cadena. Por ejemplo:
¿Por qué el último array tiene 31 elementos si solo se ven 30 letras? Lo sabremos luego.
Son arrays de tipo de dato char. Hay dos características que distinguen a estas cadenas de los
demás arrays. Primero: su inicialización se hace empleando comillas dobles y segundo, el
último término del array es un carácter NULL (simplemente un 0x00). De ahí su nombre.
Ejemplos:
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 además de las 5 letras de “Hello” se le ha añadido un
Null (0x00) al final (claro que no se nota). Es decir, la inicialización de msg es equivalente a:
char msg[] = { 'H', 'e', 'l', 'l', 'o', 0x00}; // Un array de 6 elementos
16 LOS PUNTEROS
Los punteros suelen ser el tema que más cuesta entender en programación. Pero si ya llegaste
aquí, es el momento menos indicado para detenerte.
Los punteros son un tipo de variables muy especial. Son variables que almacenan las
direcciones físicas de otras variables. Si tenemos la dirección de una variable, tenemos acceso a
esa variable de manera indirecta y podemos hacer con ellas todo lo que queramos.
Los punteros pueden apuntar a todo tipo de variables, pero no a todas al mismo tiempo. La
declaración de un puntero es un tanto peculiar. En realidad, se parece a la declaración de una
variable ordinaria solo que se pone un asterisco de por medio. En este punto debes recordar las
declaraciones de todo tipo de variables que hemos visto, incluyendo las influenciadas por los
calificadores const, static, etc. Todas excepto los arrays; ¿por qué?
data_type * PointerName;
Los siguientes ejemplos muestran lo fácil que es familiarizarse con la declaración de los
punteros:
En los siguientes ejemplos vemos cómo apuntar a variables de tipo básico, como int, char o
float. Más adelante veremos cómo apuntar a arrays.
cp = &a; // cp apunta a a
cp = &c; // Ahora cp apunta a c
cp = &a; // Ahora cp apunta a a otra vez
//...
}
16.3 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:
La expresión *p se debería leer: “la variable apuntada por p”. Eso también 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.
Luego, bastaría con modificar el valor del puntero para que apunte a los otros elementos del
array. Todo lo indicado se refleja en el siguiente código:
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 también se les
llama punteros estáticos. En la práctica 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 demás, las operaciones de asignación son similares
para punteros y arrays, tal como se puede apreciar en el siguiente código. (Por si las moscas,
str1 es el array y str2, el puntero.)
void main(void)
{
char str1[] = { 'A', 'r', 'r', 'a', 'y' };
char * str2 = { 'P', 'o', 'i', 'n', 't', 'e', 'r' };
char a;
// ...
}
Bien, recordemos: una variable pasada por valor a una función, en realidad le entrega una copia
suya; por lo que la variable original no tiene por qué ser afectada por el código de la función.
Ahora bien, pasar una variable por referencia significa que se pasa la dirección de dicha
variable. Como consecuencia, la función tendrá acceso a la variable original y podrá modificar
su contenido. Esto podría resultar riesgoso, pero, bien usada, la técnica es una potente arma.
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 29
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
Ya que los punteros operan con direcciones de variables, son el medio ideal para trabajar con
parámetros por referencia. Hay dos casos de particular interés: uno, cuando deseamos en serio
que la variable pasada a la función 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 también son punteros al fin.
En el siguiente ejemplo la función interchange intercambia los valores de las dos variables
recibidas. En seguida explicaré por qué varía un poco la forma en que se llama a la función.
Al llamar a interchange le entregamos &i y &j, es decir, las direcciones de i y j. Por otro lado, la
función interchange recibirá dichos valores en p1 y p2, respectivamente. De ese modo, p1 y p2
estarán apuntando a i y j, y podremos modificar sus valores.
Ten presente que se mantiene la forma de asignación “puntero = &variable” (puntero igual a
dirección de variable).
Ahora veamos ejemplos donde la forma de asignación cambia a “puntero = puntero”. Esto
incluye a los arrays porque, recordemos, un puntero siempre puede ser tratado como un array,
aunque lo contrario no siempre es posible.
En el siguiente programa array1 y array2 se pasan a la función prom, la cual devuelve el valor
promedio de los elementos del array recibido. Como para ese cálculo se necesita conocer la
cantidad de elementos que tiene el array, prom recibe dicho valor en el parámetro size.
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 30
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
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
mayúsculas, 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.
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 carácter 0x00.
Según la tabla de caracteres ASCII, las letras mayúsculas están ubicadas 32 posiciones por
debajo de las minúsculas. Por eso basta con sumarle o restarle ese valor a un carácter ASCII
para pasarlo a mayúscula o minúscula.
En ambas funciones el puntero p navega por los elementos del array apuntado hasta que
encuentra el final, indicado por un carácter nulo (0x00).
No es que me haya atrasado con el tema, es solo que los arrays constantes son uno de los temas
cuyo tratamiento varía mucho entre los distintos compiladores. Veamos en qué.
Un array constante es uno cuyos elementos solo podrán ser leídos pero no escritos; tan simple
como eso.
En principio, para que un array sea constante a su clásica declaración con inicialización de un
array se le debe anteponer el calificador const. No es posible declarar un array constante vacío y
llenar sus elementos después pues eso equivaldría a modificar sus elementos. Enseguida
tenemos ejemplos de declaración de arrays constantes:
De este modo, los arrays a, vocals y text serán de solo lectura, y sus elementos podrán ser
leídos, mas no escritos. El compilador mostrará mensajes de error si en lo que resta del
programa encuentra intentos de cambio, por ejemplo, como
Ahora bien, que los datos no cambien durante la ejecución del programa no necesariamente
significa los arrays constantes estén 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 dónde 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.
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 32
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
Empecemos por examinar el estilo de AVR GCC. Por ejemplo, si queremos que los tres
primeros arrays de esta página 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 añadirles al inicio la palabra reservada PROGMEM. El
calificador const 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 después. Para acceder a los elementos de estos
arrays hay que emplear una forma un tanto exótica. Se deben usar algunas macros propias del
compilador, todas proveídas por la librería pgmspace.h. Las principales son estas cuatro
Estas macros reciben como argumento la dirección de un elemento del array en FLASH. Como
la dirección de una variable cualquiera en el C se obtiene al aplicarle el operador &, las macros
citadas trabajan 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";
int var;
char c;
El manual de AVR GCC nos presenta una forma que puede resultar más fácil 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];
var = &a[1];
Por supuesto que en nuestro programa deberemos poner solo la última expresión. Es muy
importante recordar esto puesto que las expresiones previas son también válidas para el
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 33
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
compilador y no generará errores. Solo nos daremos con la sorpresa de ver nuestro programa
funcionando desastrosamente. Me pasa con frecuencia porque en mis códigos tengo la
costumbre de ubicar primero los arrays en la RAM para luego de obtener buenos resultados
mudarla a la memoria FLASH. Si eres nuevo te recomiendo seguir la misma práctica. Trabajar
con datos en FLASH desde el inicio requiere de mucha experiencia. Hay otras macros y tipos de
datos que debemos saber usar, y si no estamos seguros de lo que hacemos, repito, el compilador
no nos ayudará.
Los parámetros que reciben como argumento las macros pgm_read_byte, pgm_read_word,
pgm_read_dword y pgm_read_float son direcciones de 16 bits. Esto quiere decir que mediante
ellas podemos acceder a los arrays cuyos elementos cubren un espacio de 216 = 65536 bytes de
la memoria FLASH. En la gran mayoría de los casos es mucho más de lo que se necesita,
considerando que solo hay dos megaAVR que superan esa memoria, los ATmega128 y los
ATmega256. Pero si se presentara la descomunal situación donde tengamos que trabajar con
arrays de más de 64 KB, la librería pgmspace.h nos provee de otras macros ad hoc. Retomamos
este aspecto al final de la página.
Más que una coincidencia, lo dicho arriba es una condición necesaria para todas las variables
almacenadas en la FLASH para AVR IAR C y AVR GCC. Es decir, en estos compiladores las
variables PROGMEM deben o ser globales o static locales. Todos los ejemplos mostrados
arriba funcionan bien asumiendo que están declaradas a nivel global. Si los colocamos dentro de
una función habrá problemas.
Por ejemplo, el siguiente extracto de función no dará errores en AVR GCC pero el programa
funcionará defectuosamente, a pesar de que los arrays están declarados e inicializados conforme
a lo estudiado previamente.
/****************************************************************
* Toca las notas del ringtone apuntado por pRingtone.
***********************************************************/
void Tune(PGM_P pRingtone)
{ // C C# D D# E F F#
PROGMEM const unsigned int NoteFreqs[] = {262,277,294,311,330,349,370};
PROGMEM const unsigned char Octaves[] = {6,7,5};
PROGMEM const unsigned int Bpms[] = {0,812,406,270,203,162,135};
PROGMEM const unsigned char Durations[] = {4,8,1,2};
/* ... */
Sucede que los arrays están declarados como si fueran locales ordinarias. Si los hubiéramos
declarado globalmente estaría bien. Pero como son locales es necesario que sean además de tipo
static. Como sabemos, estas variables en C se forman añadiéndoles la palabra reservada static a
su declaración habitual. Con esto aclarado, el código anterior trabajará perfectamente si lo
escribimos de esta forma.
/************************************************************
* Toca las notas del ringtone apuntado por pRingtone.
**********************************************************/
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 34
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
Con el fin de que los programas de cursomicros sean lo más transparentes posible trato de evitar
el uso excesivo de los #defines que conducen a términos innecesarios. El hecho de estar
estudiando PGM_P sugiere que se trata de una excepción. 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 serían idénticas asumiendo que en AVR GCC const PROGMEM char
equivale a const char PROGMEM y también a PROGMEM const char, siendo esta última
presentación la forma en que hemos venido trabajando. Debido a ello en muchas ocasiones
podremos prescindir de PGM_P, pero surgirán algunos casos en que AVR IAR C muestre su
disconformidad por ese reacomodo de términos. PGM_P no solo termina de arreglar estos
desajustes sino que facilita notablemente la escritura del código ante la aparición de
construcciones más complejas como las que veremos después.
Si PGM_P define un tipo puntero que apunta a variables char (o de un byte en general), alguien
podría preguntar cuáles 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 necesario porque a fin de cuentas el puntero seguirá siendo de 16 bits. Solo suele
interesar la dirección de un dato. Para leer ese dato con el formato deseado habrá que usar la
macro adecuada, entre pgm_read_byte, pgm_read_word, pgm_read_dword y pgm_read_float,
junto a las conversiones de datos respectivos.
Ejemplo, en el siguiente programa que está escrito para los compiladores AVR IAR C y AVR
GCC Notes es un array de enteros de 16 bits y Octaves un array de enteros de 8 bits, ambos
residentes en la FLASH.
#include "avr_compiler.h"
PROGMEM const unsigned int Notes[] = {262, 277, 294, 311, 494};
PROGMEM const unsigned char Octaves[] = {6, 7, 5};
int main(void)
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 35
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
{
PGM_P p; // Declarar el puntero p
unsigned char c;
unsigned int n;
while(1);
}
Como los arrays pueden ser entendidos como punteros también, en principio se podrían hacer
las asignaciones a p directamente como p = Notes, pero para evitar protestas del compilador se
deben usar conversiones de tipo, poniendo al lado izquierdo y entre paréntesis el tipo de la
variable que recibe el valor, en este caso PGM_P porque p es de ese tipo. De ese modo p podrá
apuntar a arrays de cualquier tipo.
Por otro lado, para leer los elementos del array Octaves usamos la macro pgm_read_byte porque
es un array de bytes y para Notes usamos pgm_read_word porque es un array de enteros de 2
bytes.
A pgm_read_byte se le envía el puntero p más el índice del elemento accedido. Recordemos que
estas macros reciben direcciones y como los punteros contienen direcciones, no es necesario
sacar direcciones mediante el operador &. Este caso fue sencillo porque el tipo de Octave se
acoplaba fácilmente a PGM_P.
El hecho de usar el tipo PGM_P hace presuponer que solo se trabajará con variables de bytes
que son accedidas mediante la macro pgm_read_byte. De hecho es así en la gran mayoría de los
casos y todo queda bien. La legibilidad se pierde en programas como el ejemplo previo donde el
mismo puntero se usa también para acceder a un array de enteros de 2 bytes. Si de todos modos
vamos a estar haciendo conversiones de tipos lo más recomendable sería usar un puntero
"neutro" lo cual deja por sentado que trabajará sobre variables de distintos tipos.
Ese tipo de puntero existe y se llama PGM_VOID_P. Es aceptado así en los compiladores AVR
IAR C y AVR GCC. Es un puntero a void definido en AVR GCC como const void PROGMEM
* y en AVR IAR C como const void __flash *. Lo importante es que su empleo es similar al
puntero PGM_P, así que lo asimilaremos de inmediato. El programa anterior por ejemplo se
puede reescribir de la siguiente forma. (El código quedó con mejor acabado y con una linda
simetría.)
#include "avr_compiler.h"
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
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 36
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
unsigned char c;
unsigned int n;
while(1);
}
Los punteros PGM_P y PGM_VOID_P también 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 demostración abarcaría demasiado
espacio. Prefiero remitirme a la librería para USB que distribuye Atmel. Me parece un perfecto
ejemplo. Puedes encontrarla en varias notas de aplicación como AVR270, AVR271, AVR272 y
AVR273, por citar algunas. En el archivo usb_standar_request.c se declara y usa el puntero
pbuffer de tipo PGM_VOID_P para acceder a los descriptores de USB que por su tamaño
residen en la FLASH.
Esa librería USB se vale de un archivo llamado compiler.h para guardar la compatibilidad de
códigos 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 librería. Igual vale la pena revisarla.
En primer lugar recordemos que los argumentos de las funciones deben ser del mismo tipo que
las variables que se le envían. Si las variables son residentes en la FLASH, lo cual deja suponer
que son arrays o estructuras complejas, el método a usar son los punteros, no solo por el tamaño
de esas variables sino por la capacidad de adaptación de los punteros que estudiamos en la
sección anterior.
En el siguiente programa la función print imprime un mensaje por el puerto serie, similar a puts
de la librería stdio.h del compilador. La función puts solo trabaja con mensajes en la RAM a
diferencia de print que recibe arrays residentes en la FLASH. Los dos compiladores que usamos
también ofrecen funciones de FLASH y a eso pretendemos llegar: a su uso.
#include "avr_compiler.h"
#include "usart.h"
/*************************************************************
* Envía por el USART el texto pasado en p
*******************************************************/
void print(PGM_P p)
{
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 37
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
char c;
while( (c = pgm_read_byte(p++)) != 0x00 )
putchar(c);
}
/*******************************************************
* Main function
***********************************************************/
int main(void)
{
usart_init(); // Inicializar USART
print(rt01); // Imprimir mensaje de rt01
print(rt02); // ...
print(rt03); // ...
print(rt04); // ...
while (1);
}
Creo que el código 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
conversión de tipo. La conversión de tipo para p es opcional, por ejemplo, también se pudo
escribir print((PGM_P)rt01).
Y ahora la pregunta que nos trae aquí: ¿Se puede enviar a una función 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.
El compilador AVR GCC todavía acepta las sentencias y construye el programa limpiamente,
sin presentar errores, ni siquiera advertencias. Pero el resultado es un programa mostrando
mamarrachos en vez de los villancicos esperados. El compilador AVR IAR C, por su parte,
simplemente no admite el código fuente. ¿Qué pasó?
Como variables locales ordinarias que son, los compiladores tratan de implementar las cadenas
pasadas en la memoria RAM. AVR IAR C nota la incompatibilidad de tipos y rechaza el código
de plano, en tanto que AVR GCC sí cumple el cometido pasando por alto la divergencia de
tipos porque, recordemos, este compilador hace la diferencia en el momento de acceder a las
variables usando sus macros como pgm_read_byte. Esa macro recibe en el programa una
dirección RAM (también de 16 bits) y la usa para leer de la memoria FLASH como si las
cadenas de texto estuvieran allí. Lee "cualquier cosa" menos las cadenas.
Alguien más avezado podría decir que eso se puede arreglar con conversiones de tipos por
ejemplo reescribiendo las sentencias así
El código vuelve a compilarse limpiamente. Hasta AVR IAR C es engañado. Pero cuando
vemos el programa en acción descubrimos que solo nos hemos engañado a nosotros mismos. En
este programa los garabatos que se visualizan en el terminal serial nos quitaron la venda de los
ojos rápidamente, felizmente. En otras circunstancias detectar el error hubiera costado más. Las
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 38
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
cadenas siguen siendo colocadas en la RAM. Recordemos que para que las variables residan en
la FLASH deben o ser globales o static locales. Lo primero es obviamente un imposible; y lo
segundo, que sean static, solo es posible en el compilador AVR GCC gracias a una macro
llamada PSTR que inicializa el array como static y toma su dirección automáticamente. La
siguiente construcción entonces funcionará como se desea pero solo en este compilador.
Solo por curiosidad, la macro PSTR tiene la siguiente definición. PSTR es ampliamente usada
cuando se trabaja con las funciones _P del compilador AVR GCC. Así que hablaremos más de
ella en adelante.
Para crear un array de cadenas en FLASH primero se declaran e inicializan las cadenas de la
forma ya conocida y luego se construye el array con los nombres de las cadenas. Esta regla es
única, inflexible, limitante e igualmente válida para los dos compiladores que usamos, AVR
IAR C y AVR GCC. Con un ejemplo lo vamos a entender mejor.
El objeto del siguiente programa es idéntico al ejemplo de la sección anterior: el programa debe
mostrar los mismos mensajes almacenados en la FLASH solo que esta vez se les desea acceder
mediante un índice, por eso los mensajes están agrupados en un array.
#include "avr_compiler.h"
#include "usart.h"
PGM_P ringtones[] =
{
ringt01,
ringt02,
ringt03,
ringt04
};
/****************************************************************
* Main function
***************************************************************/
int main(void)
{
usart_init(); // Inicializar USART
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 39
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
while (1);
}
La función puts_P es proveída por los compiladores. Es similar a la función puts pero las
cadenas que recibe deben ubicarse en la FLASH. En otras palabras, puts_P es similar a la
función print que 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
más elementos en el array ringtones, la situación cambiaría y sería mejor que también residiera
en la FLASH. Eso lo veremos al final.
Elaborar el array y el contenido de sus elementos por separado es incómodo 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. Quisiéramos que fuera posible implementar el array por
ejemplo como se muestra abajo donde cada elemento se inicializa directamente, pero eso solo es
factible en otros compiladores como CodeVisionAVR o SDCC. Por lo que dicen sus manuales,
en los compiladores AVR IAR C y AVR GCC en el mejor de los casos esto solo ubicará los
elementos en la RAM. En AVR GCC ni siquiera la macro PSTR, que sirve para inicializar en
línea datos residentes en FLASH, podrá ayudarnos esta vez. Y, lo olvidaba, no intentes forzar el
destino del array o de sus elementos utilizando conversiones de tipos. Solo evadirás los errores
y advertencias, pero los datos seguirán yendo a la RAM y el programa funcionará
incorrectamente. (Haz clic aquí si quieres ver la versión CodeVisionAVR de este programa.)
PGM_P ringtones[] =
{
"\r Deck the halls",
"\r Jingle bells",
"\r We wish you a merry christmas",
"\r Silent night"
};
Esa presentación coincide con la forma en que hemos estado trabajando antes: primero
PROGMEM, luego const y al final el tipo de dato. En AVR IAR C una permutación a veces
producirá incompatibilidades. Por tanto, el uso de PGM_P más que una cuestión de
simplificación es una necesidad que facilitará la compatibilidad de códigos.
Como dijimos antes, cada elemento del array ringtones es un puntero de 2 bytes y en conjunto
no ocupan mucha RAM en este programa. Ahora bien si nuestro código requiriera incluso ese
espacio para otros datos o si el array es bastante más grande, entonces el mismo array ringtones
también debería almacenarse en la memoria FLASH. Solo hay un arreglo para esto y, como ya
discutimos demasiado, vamos directamente a poner la forma que debe tener el programa en ese
caso.
#include "avr_compiler.h"
#include "usart.h"
ringt04
};
/*******************************************************
* Main function
************************************************/
int main(void)
{
usart_init(); // Inicializar USART
while (1);
}
La conversión 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.
Cambiando de tema, en AVR GCC el programa se compila en 528 bytes de memoria FLASH y
20 bytes de RAM. En CodeVisionAVR había tomado 490 bytes de FLASH y solo 8 bytes de
RAM. Es uno de los inusuales casos donde gana CodeVisionAVR, normalmente ni se le acerca.
Pero AVR IAR C lo hizo en 375 bytes de FLASH y 70 bytes de RAM. Parece que AVR IAR C
hubiera puesto los datos en la RAM, pero no. Es el estilo de este compilador tomar un poco más
de RAM para ahorrar más FLASH. En los tres casos la compilación se realizó con la
optimización a máximo nivel.
Sin ser muy observadores notamos que sus nombres provienen de las macros anteriores. Ahora
llevan el sufijo _far. Estas macros reciben como argumento direcciones de 32 bits con lo que
teóricamente tienen un alcance de hasta 4 GB de datos en FLASH. Como en la práctica los
punteros X, Y y Z de los AVR de 8 bits solo llegan a ser de 24 bits, su alcance es en realidad de
16 MB. El uso de estas macros es completamente igual al de sus pares de 16 bits, por eso se ven
como redundantes los siguientes ejemplos. Las variables sobre las que actúan también deben ser
declaradas con PROGMEM.
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";
int var;
char c;
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 41
UNIVERSIDAD NACIONAL DEL CALLAO FIEE - 2014V MICROCONTROLADORES
-----------------------------------------------------------------------------------------------------------------------------
Podemos entender que los únicos AVR que aceptan estas macros son los que tienen más 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 serán igual de eficientes. Si el código de arriba, por
ejemplo, estuviera escrito para un ATmega1284P, el acceso se dilataría 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 inglés), en la
librería pgmspace.h también hay macros con el apéndice _near (cerca). Estas nuevas macros son
pgm_read_byte_near, pgm_read_word_near, pgm_read_dword_near y pgm_read_float_near.
Pero no te preocupes si crees que el tema se va recargando demasiado. No se tratan más 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 demás. Es bueno saberlo
para no quedar sorprendidos por los códigos de quienes prefieren usar la una u otra forma.
------------------------------------------------------------------------------------------------------------------------------
M.S.c Ing. Jacob Astocondor Villar 42