Lenguaje C para Microcontroladores > Los compiladores de alto nivel
Introducción Los compiladores de alto nivel Los compiladores de alto nivel son las herramientas de programación mas potentes que existen. En resumidas cuentas, el compilador traducirá el programa escrito en su lenguaje (C, Basic, Pascal u otro) en código ensamblador y luego generará los archivos de depuración (*.dbg, *.cof, *.d31, etc.) necesarios y de ejecución (*.hex). Al trabajar con un lenguaje de alto nivel el programador ya no tiene que preocuparse (o lo hace muy poco) por las características hardware ni por el ensamblador nativo de cada microcontrolador. Esto simplifica de manera asombrosa el desarrrollo de proyectos. Los compiladores se encargan de traducir el código fuente al código objeto de cada microcontrolador sin inportar mucho cuál sea. Por ejemplo, un código escrito para un PIC16F84 podría ser facilmente compilado para un PIC16F877A u otro, y viceversa. Inclusive es posible adaptar un código para un microcontrolador de otra marca, por ejemplo, de Freescale o Atmel. Eso se llama portabilidad.
¿Por qué C y no Basic? Ciertamente, el Basic es el lenguaje más fácil de aprender (no es exactamente la razón de su nombre). Y aunque los programadores en C de ordenadores miren con desdén a los que usan el Basic, en el mundo de los microcontroladores los compiladores Basic no tienen motivo para sentirse menos. De hecho, algunos pueden ser casi tan eficientes como los mejores compiladores C. Las características (muchas veces complejas) del C fueron ideadas para el trabajo con sofisticados proyectos, propios de los ordenadores. Muchas de esas características ya no resultan tan ventajosas en el limitado hardware de los microcontroladors y se convierten en prescindibles. Además, la simplicidad de los compiladores Basic para microcontroladores también permite que varios de ellos, como MBasic o PIC Basic Pro (por citar algunos) mantengan una compatibilidad entre sus códigos que no se encuentra entre los compiladores C. Ésas podrían ser razones más que convincentes para empezar por el Basic y, de hecho, es la opción que muchos han elegido. ¿Por qué nosotros no? Porque es verdad comprobable que los mejores programadores trabajan en C (no siempre exclusivamente, pero lo manejan). Por consiguiente, los proyectos más fantásticos y alucinantes que se pueden encontrar están en C. Es más, la mayoría de, por no decir todos, los programadores de Basic tarde o temprano se ven obligados a aprender el C. No sé tú, pero yo opino que esa razón pesa más. Además, dada la robustez y la aceptación del lenguaje C, se lo ha tomado como referencia para lenguajes de otros propósitos como Java, JavaScript, php o de Matlab, entre otros. Así que, el C podrá servirte para trabajar en otros campos. El programador de C podría, inclusive, aprender luego el Basic sin el menor esfuerzo; lo contrario no es cierto.
¿Qué compilador C utilizar?
No quiero burlarme de nadie, pero una vez leí en Internet el comentario de un novato: “Quiero programar microcontroladores en C. Ya descargué el Visual C++. ¿Qué más necesito?” :). Aparte del lenguaje, nada tiene que ver un compilador para ordenadores con los compiladores para µCs. Poco tiene que ver un compilador para PICs que otro para otros µCs. Inclusive, poco tiene que ver un compilador de PICs de una compañía con otro de otra compañía. Veamos grosso modo algunos aspectos de los compiladores de PICs más conocidos. Hi-tech C. Es uno de los compiladores producidos por la empresa htsoft. Es quizá el más eficiente y el que mejor soporta el lenguaje C estándar. Su entorno IDE también incluye el mejor depurador ICD. Como contraparte, su apego al hardware del µC le resta algo de portabilidad. Tampoco luce librerías incorporadas como otros productos. Pero su principal desventaja es su elevado precio. Y, por si fuera poco, el compilador para la familia de partes PIC18 se vende por separado. IAR C. Los compiladores C de la compañía iar systems tienen básicamente las mismas características mencionadas de los compiladores de htsoft, incluyendo sus propios depuradores. Así mismo, las versiones para los PIC16 y PIC18 se distribuyen por separado. Actualmente, no sé por qué, ya no está disponible la primera versión. CCS C. La empresa ccsinfo decidió dotar a sus compiladores C una capa extra que aísla al programador de los recursos intrínsecos del µC. Esto puede afectar la portabilidad de sus códigos a otros compiladores, pero resulta inmejorable, si solo se trabaja en el lenguaje de CCS C, para transportar los códigos de un PIC a otro (de cualquier familia) con un esfuerzo sin comparación. Además, incluye en un solo paquete los compiladores para los PICs de las familias Baseline, Midrange (PIC16 básicamente) y High performance (PIC18). Al igual que los softwares anteriores, sus librerías estándar, como stdlib.h, stdio.h, string.h y math.h, son muy completas y potentes; pero CCS C supera a sus rivales al incorporar librerías para controlar todos los módulos internos del PIC y también muchísimos dispositivos externos. Mikro C. La compañía Mikroelektronika vende compiladores para PICs en los lenguajes C (MikroC), Basic (MikroBasic) y Pascal (MikroPascal). Yo diría que el estilo de Mikro C se parece al de Hi-tech C y sus facilidades tratan de acercarse a las de CCS C: aunque en muchos casos aún es necesario acceder a los registros internos del PIC, cuenta con librerías para controlar sus módulos internos. También tiene una apreciable cantidad de librerías para interfacear dispositivos externos. Lo malo es que todas ellas están precompiladas y no se podrían modificar, en caso de ser necesario. Mikroelektronika y CCS también comercializan sus propias tarjetas de entrenamiento para el aprendizaje de sus productos. Para más información puedes visitar sus sitios web. MPLAB C18. Excelente compilador desarrollado por los ingenieros de Microchip. No es gratuito como el MPLAB, pero creo que es el que ofrece la versión demo más generosa: es 100 % funcional por 60 días. Lamentablemente, como sugiere su nombre, solo trabaja con las partes PIC18. Quizá lo probemos en otro momento. Otros. Aún hay otros compiladores C (como Bytecraft, BoostC y FedC) que algo menos reconocidos como los anteriores, lo que no significa que sean malos.
También he visto algunos de código abierto, pero no son buenos: la gente del GNU trabaja más con el AVR GCC, un “Señor Compilador”. Es uno de los pocos casos donde el software libre supera a los comerciales. Como se puede entrever, está orientado a los microcontroladores AVR, de Atmel. Es, además, el compilador más difícil de todos; por eso lo estudiaremos en el Módulo 4. En cuanto a cuál compilador usar: la idea de este curso no es aprender a programar con un compilador en particular, y tampoco pretendo promocionar alguno. Después de todo, una victoria depende más de la habilidad guerrero que de su espada. He visto “super programas” hechos con el compilador más modesto. En este Módulo 2 uso BoostC porque es muy fácil, porque nos permitirá ver más de cerca cómo funcionan las cosas dentro del PIC y, sobre todo, porque el salto de él a otros compiladores será mucho más fácil que hacerlo al revés. En el Módulo 3 migraremos al CCS C (que además del lenguaje C usa su propio “argot”) y en el Módulo 4 trabajaremos especialmente con AVR GCC.
¡Ánimo! No es tan difícil Pienso que, comparado con el Basic para microcontroladores, el C es infinitamente más difícil de aprender. Quienes lo usan, en gran parte, son personas que han tenido experiencia programando ordenadores, personas que han estudiado más de un libro para dominarlo. Es, literalmente, como aprender un nuevo idioma, y eso no es algo que se hace de la noche a la mañana. ¿Eso no suena muy alentador? Para simplificar las cosas, en este capítulo no voy a exponer todas las reglas del lenguaje C, aunque sí la mayoría; digamos el 95 % de lo necesario. El resto: o es solo aplicable a los PCs, o son temas raros o que difieren demasiado entre de compilador a otro y conviene más revisarlos en sus respectivos manuales. También, y para ahorrar los ejemplos prácticos, asumo que no eres un novato cualquiera, asumo que conoces algo de programación (aunque sea en ensamblador), que sabes cómo usar las subrutinas, que sabes cómo emplear los bucles, que sabes lo que significa redirigir el flujo de un programa, que sabes para qué sirven las variables, etc. Si no, estarás algo perdido. Finalmente, no es necesario que te aprendas de golpe todo el capítulo; bastará con que lo leas fluidamente una primera vez y regresar luego a consultar algunos puntos de duda. La parte más complicada es Arrays y Punteros, sobre todo los punteros. Así que, ten paciencia con ellos.
Estructura de un programa en C Tomaremos en cuenta este sencillísimo ejemplo, escrito para el compilador Hitech PICC. #include
// Incluir este archivo
/* La siguiente directiva establece la Palabra de Configuración */ __CONFIG ( PWRTEN & WDTDIS & XT & UNPROTECT ); void pausa(void) { unsigned int c;
// Llave de apertura del bloque de pausa // Declarar variable c (de 16 bits)
for(c=0; c<60000; c++) { // Llave de apertura del bloque de for /* este bloque está vacío, solo cuenta c desde 0 hasta 59999 */ } // Llave de cierre del bloque de for
}
// Llave de cierre del bloque de pausa
void main(void) { TRISB0 = 0; while(1) { RB0 = 1; pausa(); RB0 = 0; pausa(); } }
// // // // // // // // // //
Llave de apertura del bloque de main Configurar pin RB0 como salida Bucle infinito Llave de apertura del bloque de while Setear bit RB0 Llamar función pausa Limpiar bit RB0 Llamar función pausa Llave de cierre del bloque de while Llave de cierre del bloque de main
No hay que ser muy perspicaz para descubrir lo que hace este programa: configura el pin RB0 como salida y luego lo setea y lo limpia tras pausas. Es como hacer parpadear un LED conectado al pin RB0. Parpadea porque el bloque de while se ejecuta cíclicamente. Los elementos más notables de un programa en C son las sentencias, las funciones, las directivas, los comentarios y los bloques. A continuación, una breve descripción de ellos.
Los comentarios Los comentarios tienen el mismo propósito que en ensamblador: documentar y “adornar” el código. Es todo es texto que sigue a las barritas // y todo lo que está entre los signos /* y */. Se identifican fácilmente porque suelen aparecer en color verde. Ejemplos. // Éste es un comentario simple /* Ésta es una forma de comentar varias líneas a la vez. Sirve mucho para enmascarar bloques de código. */
Las sentencias Un programa en C, en lugar de instrucciones, se ejecuta por sentencias. 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). Por ejemplo, la función main del programa de arriba bien pudo escribirse del siguiente modo. void main(void) { TRISB0=0; while(1) { RB0=1; pausa(); RB0=0; pausa(); } }
¿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.
Las sentencias se pueden clasificar en sentencias de asignación, sentencias selectivas, sentencias iterativas, de llamadas de función, etc. Las describiremos más adelante.
Los bloques 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.
Las directivas Son conocidas en el lenguaje C como directivas de preprocesador, de preprocesador porque son evaluadas antes de compilar el programa. Como pasaba en el ensamblador, las directivas por sí mismas no son código ejecutable. Suelen ser indicaciones sobre cómo se compilará el código. Entre las pocas directivas del C estándar que también son soportadas por los compiladores C para PICs están #include (para incluir archivos, parecido al assembler), #define (mejor que el #define del ensamblador) y las #if, #elif, #endif y similares. Fuera de ellas, cada compilador maneja sus propias directivas y serán tratadas por separado.
Las funciones Si un programa en ensamblador se puede dividir en varias subrutinas para su mejor estructuracion, un programa en C se puede componer de funciones. Por supuesto que las fuciones son muchísimo más potentes y, por cierto, algo más complejas de aprender. Por eso ni siquiera el gran espacio que se les dedica más adelante es suficiente para abarcarlas. 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 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.
Variables y Tipos de Datos En ensamblador todas nuestras variables de programa eran 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: Tipo de dato Tamaño char 8
Rango 0 a 255 ó -128 a 127
Tipo de dato Tamaño Rango signed char 8 -128 a 127 unsigned char 8 0 a 255 (signed) int 16 -32,768 a 32,767 unsigned int 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 float 32 +/- 1.18E–38 a +/- 3.40E+38 Por desgracia, excepto signed char y unsigned char, los otros tipos establecen variables de tamaños y/o rangos que suelen varíar de un compilador C a otro. Otros compiladores también manejan los tipos short, double, bool (o boolean), bit, etc. Esas divergencias pueden afectar la portabilidad de los códigos, además de confundir a los programadores. Los valores de esta tabla son los utilizados por la mayoría de los compiladores C. 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.
Declaración de variables Esta parte es comparable, aunque lejanamente a cuando identificábamos nuestras variables del ensamblador con las directivas equ o cblock – endc. 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; char b; signed char c; int i; signed int j; unsigned int k;
// // // // // // //
Variable para enteros de 8 bits sin signo Variable de 8 bits (para almacenar caracteres ascii) Variable para enteros de 8 bits con signo i es una variable int, con signo j también es una variable int con signo 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: float area, side; unsigned char a, b, c;
// Declarar variables area y side de tipo float // Declarar variables a, b y c como unsigned char
Especificadores de tipo de datos A la declaración de una variable se le puede añadir un especificador de tipo como const, static, volatile, extern, register, etc. Dichos especificadores tienen diversas funciones y, salvo const, se suelen usar en programas más elaborados. Como no queremos enredarnos tan pronto, lo dejaremos para otro momento.
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: const int a = 100; int b;
// Declarar constante a // Declarar variable b
//... b = a; b = 150;
// Válido // Válido
a = 60; a = b;
// Error! a es constante // Error! a es constante
Por más que las variables constantes sean de solo lectura, ocuparán posiciones en la RAM del µC. Por eso muchas veces es preferible definir las constantes del programa con las clásicas directivas #define (como lo hacíamos en el ensamblador). #define a 100
// Definir constante a
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.
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.
Diagrama de flujo de la sentencia if. La forma codificada sería así:
sentenciaA; if ( expression ) {
// Si expression es verdadera, // ejecutar el siguiente bloque // apertura de bloque
sentenciaB; sentenciaC; // algunas otras sentencias } // cierre de bloque sentenciaX;
Después de ejecutar sentenciaA el programa evalúa 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 condición lógica 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.
Diagrama de flujo de la sentencia if – else. Expresando lo descrito en código C, tenemos: (Se lee como indican los comentarios.) SentenciaA; if ( expression ) { sentenciaB; sentenciaC; // ... } else { sentenciaM; sentenciaN; // ... } sentenciaX; // ...
// Si expression es verdadera, ejecutar // este bloque
// En caso contrario, ejecutar este bloque
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.
La sentencia if – else – if escalonada Es la versión ampliada de la sentencia if – else. 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. if ( expression_1 ) // Si expression_1 es verdadera ejecutar { // este bloque sentencia1; sentencia2; } else if ( expression_2 ) // En caso contrario y si expression_2 es { // verdadera, ejecutar este bloque sentencia3; sentencia4; } else if ( expression_3 ) // En caso contrario y si expression_3 es { // verdadera, ejecutar este bloque sentencia5; sentencia6; } else // En caso contrario, ejecutar este bloque { sentencia7; sentencia8; }; // ; opcional // todo...
Las “expresiones” se evualú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.
La sentencia switch La sentencia switch brinda una forma más elegante de bifurcación múltiple. Podemos considerarla como una forma más estructurada de la sentencia if – else – if escalonada, aunque tiene algunas restricciones en las condiciones lógicas a evaluar, las cuales son comparaciones de valores enteros. Para elaborar el codigo 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.) expresion 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. No sería necesario poner el default si su bloque estuviera vacío.
Sentencias iterativas 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 mas 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 inglés) una expresión sea verdadera.
Diagrama de flujo de las sentencia while. El bucle while en C tiene la siguiente sixtaxis 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; // ... }; sentenciaX; // ...
// Este ; es opcional
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”.
La sentencia do - while Como dije antes, es una variación de la sentencia while simple. La principal diferencia es que la condición lógica (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.
Diagrama de flujo de las sentencia do – while. La sintaxis para la sentencia do – while es la siguiente y se lee: Ejecutar (do) el siguiente bloque, mientras (while) expression sea verdadera. sentenciaA; do { sentenciaB; sentenciaC; // ... } while ( expression ); // Este ; es mandatorio sentenciaX; // ...
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 algún 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
Ahora veamos por partes cómo funciona:
expression_1 suele ser una sentencia de inicialización. expression_2 se evualúa como condición lógica para que se ejecute el bloque. expression_3 es una sentencia que debería poner coto a expression_2.
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): 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 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. 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 más construcciones, todas coincindentes con la primera plantilla, pero también 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. Por ejemplo, las siguientes sentencias: if(a > b) {
a = 0; } if(a == b) { a++; } else { b--; } while( a >= b) { a = a + b; } for(i=0; i<=10; i++) { a = a*2; }
bien se pueden escribir de la siguiente forma: if(a > b) a = 0; if(a == b) a++; else b--; while( a >= b) a = a + b; for(i=0; i<=10; i++) a = a*2;
Los operadores Sirven para realizar operaciones aritméticas, lógicas, comparativas, etc. Según esa función se clasifican en los siguientes grupos.
Operadores aritméticos Además de los típicos operadores de suma, resta, multiplicacion y división, están los operadores de módulo, incremento y decremento. Operador Acción + Suma Resta * Multiplicación / División % Módulo. Retorna el residuo de una división entera. Solo se debe usar con números enteros.
Operador ++ Incrementar en uno -Decrementar en uno
Acción
Ejemplos: int a, b, c;
// Declarar variables a, b y c
a = b + c; // Sumar a y b. Almacenar resultado en c b = b * c; // Multiplicar b por c. Resultado en b b = a / c; // Dividir a entre c. Colocar resultado en b a = a + c – b; // Sumar a y c y restarle b. Resultado en a c = (a + b) / c; // Dividir a+b entre c. Resultado en c b = a + b / c + b * b; // Sumar a más b/c más b×b. Resultado en b c = a % b; // Residuo de dividir a÷b a c a++; // Incrementar a en 1 b--; // Decrementar b en 1 ++c; // Incrementar c en 1 --b; // Decrementar b en 1
¿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.
Si ++ o -- están antes del operando, primero se suma o resta 1 al operando y luego se evalúa la expresión. Si ++ o -- están después del operando, primero se evalúa la expresión y luego se suma o resta 1 al operando. int a, b;
// Declarar variables enteras a y b
a = b++; a = ++b;
// Lo mismo que a = b; y luego b = b + 1; // Lo mismo que b = b + 1; y luego a = b;
if (a++ < 10) { // algún código }
// Primero comprueba si a < 10 y luego // incrementa a en 1
if (++a < 10) { // algún código }
// Primero incrementa a en 1 y luego // comprueba si a < 10
Operadores de bits Se aplican a operaciones lógicas con variables a nivel binario. Aquí tenemos las clásicas operaciones AND, OR inclusiva, OR exclusiva y la NEGACIÓN. Adicionalmente, he incluido en esta categoría los operaciones de desplazamiento a la derecha y la izquierda.
Si bien son operaciones que producen resultados análogos a los de las instrucciones de ensamblador iorlw y iorwf para la OR inclusiva, xorlw y xorwf para la OR exclusiva, andlw y andwf para la AND y comf para la negación; los operadores lógicos del C pueden operar sobre variables de distintos tamaños, ya sean de 1, 8, 16 ó 32 bits. Operador Acción & AND a nivel de bits | OR inclusiva a nivel de bits ^ OR exclusiva a nivel de bits ~ Complemento a uno a nivel de bits << Desplazamiento a la izquierda >> Desplazamiento a la derecha Ejemplos: char m; int n; m m m m n n m m m
= = = = = = = = =
0x48; m & 0x0F; m | 0x24; m & 0b11110000; 0xFF00; ~n; m | 0b10000001; m & 0xF0; m ^ 0b00110000;
m = 0b00011000; m = m >> 2; n = 0xFF1F; n = n << 12; m = m << 8;
// variable de 8 bits // variable de 16 bits // // // // // // // // //
m será 0x48 Después de esto m será 0x08 Después de esto m será 0x2F Después de esto m será 0x20 n será 0xFF00 n será 0x00FF Setear bits 0 y 7 de variable m Limpiar nibble bajo de variable m Invertir bits 4 y 5 de variable m
// Cargar m con 0b00011000 // Desplazar m 2 posiciones a la derecha // Ahora m será 0b00000110 // Desplazar n 12 posiciones a la izquierda // Ahora n será 0xF000; // Después de esto m será 0x00
Fíjate en la semejanza entre las operaciones de desplazamiento con >> y << y las operaciones del rotación del ensamblador. La diferencia es que 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.
Desplazamientos producidos por los operadores << y >>.
Operadores relacionales 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.
Operador == != > < >= <=
Acción Igual No igual Mayor que Menor que Mayor o igual que Menor o igual que
Operadores lógicos 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. OperadorAcción && || !
AND lógica OR lógica Negación lógica
Ejemplos: if( !(a==0) ) { // sentencias }
// Si a igual 0 sea falso
if( (ac) ) { // sentencias }
// Si ac son verdaderas
while( (a==0) || (b==0) ) { // sentencias }
// Mientras a sea 0 ó b sea 0
Composición de operadores 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: obtect op= expression;
que es equivalente a la sentencia object = object op expression;
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; a += 50; a += 20; a *= 2; a &= 0xF0; a <<= 1;
// // // // // //
Declarar a Es lo mismo que a También significa Es lo mismo que a Es lo mismo que a Es lo mismo que a
= a + 50; sumarle 20 a a = a * 2; = a & 0xF0; = a << 1;
Precedencia de operadores Una expresión puede contener varios operadores, de esta forma: 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é. Por ejemplo, las tres siguientes sentencias son diferentes. b = (a * b) + (c / b); b = a * (b + (c / b)); b = ((a * b) + c)/ b);
También se pueden construir expresiones condicionales, así: if ( (a > b) && ( b < c) ) { // ... }
// Si a>b
y
b
Las funciones 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. data_type1 function_name (data_type2 arg1, data_type3 arg2, ... ) { // Cuerpo de la función // ...
return SomeData;
// Necesario solo si la función retorna algún valor
}
Donde:
function_name es el nombre de la función. Puede ser un identificador cualquiera. data_type1 es un tipo de dato que identifica el parámetro de salida. Si no lo hubiera, se debe poner la palabra reservada void (vacío, en inglés). arg1 y arg2 (y puede haber más) son las variables de tipos data_type1, data_type2..., respectivamente, que recibirán los datos que se le pasen a la función. Si no hay ningún parámetro de entrada, se pueden dejar los paréntesis vacíos o escribir un void entre ellos.
Funciones sin parámetros Para una función que no recibe ni devuelve ningún valor, la plantilla de arriba se reduce al siguiente esquema: void function_name ( void ) { // Cuerpo de la función }
Y se llama escribiendo su nombre seguido de paréntesis vacíos, así: 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. void main (void) { // Cuerpo de la función }
Funciones con parámetros (por valor) De momento, solo estudiaremos las funciones que pueden tener varios parámetros de entrada pero solo uno de salida. Si la función no tiene parámetros de entrada o de salida, debe escribirse un void en su lugar. El valor devuelto por una función se indica con la palabra reservada return. Según el comportamiento de los parámetros de entrada de la función, estos se dividen en parámetros por valor y parámetros por referencia. Lo expuesto en este apartado corresponde al primer grupo porque es el caso más ampliamente usado. Con esto en mente podemos seguir. 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.
int minor ( int arg1, int arg2, int arg3 ) { int min; // Declarar variable min min = arg1; // Asumir que el menor es arg1 if ( arg2 < min ) min = arg2;
// Si arg2 es menor que min // Cambiar a arg2
if ( arg3 < min ) min = arg3;
// Si arg3 es menor que min // Cambiar a arg3
return min; } void main (void) { int a, b, c, d;
// Retornar valor de min
// Declarar variables a, b, c y d
/* Aquí asignamos algunos valores iniciales a 'a', 'b' y 'c' */ /* ... */ d = minor(a,b,c); // Llamar a minor // En este punto 'd' debería ser el menor entre 'a', 'b' y 'c' while (1); // Bucle infinito }
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. El mecanismo funciona así: siempre respetando el orden, al llamar a minor el valor de a se copiará a la variable arg1; el valor de b, a arg2 y el valor de c, a arg3. Después de ejecutarse el código de la función, el valor de retorno (min en este caso) será copiado a una variable temporal y de allí pasará a d. 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: d = minor(-15, 100, 5.124); // Llamar a minor
Aquí los dos primeros parámetros están bien, pero el tercero es un número decimal (de 24 ó 32 bits), no compatible con el tercer parámetro que la función espera (entero de 8 ó 16 bits). En estos casos el compilador nos mostrará mensajes de error, o cuando menos de advertencia.
Parámetros por referencia 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.
Prototipos de funciones 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 void func1(char m, long p); // Prototipo de función "func1" char func2(int a); // Prototipo de función "func2" void main(void); // Prototipo de función "main". Es opcional 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 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 // Desde aquí se puede acceder a func1 y main }
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 void func1(char, long); char func2(int );
Variables locales y variables globales 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. En la mayoría de los compiladores C para PICs las variables locales deben declararse al principio de la función. 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. char foo(long );
// Prototipo de función
int speed; // Variable global const long limit = 100; // Variable global constante void inter(void) { int count; // Variable local /* Este count no tiene nada que ver con el count de las funciones main o foo */
speed++;
// Acceso a variable global speed
vari = 0;
// Esto dará ERROR porque vari solo pertenece // a la función foo. No compilará.
} void main(void) { int count; // Variable /* Este count no tiene nada que de las funciones inter o foo count = 0; // Acceso a speed = 0; // Acceso a } char foo(long count) // Variable { int vari; // Variable }
local count ver con el count */ count local variable global speed local count local vari
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.
Variables static Antes de nada debemos aclarar que una variable static local tiene diferente significado que una variable static global. Ahora vamos a enfocarnos al primer caso por ser el más común. 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; a++; }
// Declarar variable 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 autoinicializan 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í: static int var1; static int var2 = 50;
// Variable static (inicializada a 0 por defecto) // Variable static inicializada a 50
Ejemplos. void increm() { static int a = 5; // Variable local estática inicializada a 5 a++; // Incrementar a } void main() { int i; // Declarar variable i // El siguiente código llama 10 veces a increm for(i=0; i<10; i++) increm(); // Ahora la variable a sí debería valer 15 while(1); // Bucle infinito }
Variables volatile A diferencia de los ensambladores, los compiladores tienen cierta “inteligencia”. Es decir, piensan un poco antes de traducir el código fuente en código ejecutable. Por ejemplo, veamos el siguiente pedazo de código para saber lo que suele pasar con una variable ordinaria: int var; //... var = var;
// Declarar variable var // Asignar var a var
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. volatile int count;
// count es variable global volátil
void interrupt(void) // Función de interrupción { // Código que accede a count } void main(void) // Función principal { // Código que accede a count }
Arrays y Punteros 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 nada, perdería la potencia por la que las mejores empresas lo eligen para crear sus softwares de ordenadores. 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.
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 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.
Estructura de un array unidimensional de N elementos.
Declaración de arrays Para declarar un array unidimensional se utiliza la siguiente sintaxis: data_type identifier[ NumElementos ];
Donde data_type es un tipo de dato cualquiera, identifier es el nombre del array y NumElementos es la cantidad de elementos que tendrá (debe ser un valor constante). De este modo, el índice del primer elemento es 0 y el del último es NumElements - 1.
Por ejemplo, las siguientes líneas declaran tres arrays. char letters10]; long HexTable[16]; int address[100];
// letters es un array de 10 elementos de tipo char // HexTable es un array de 16 elementos de tipo long // address es un array de 100 elementos de tipo int
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í: letters[0] letters[1] letters[2] letters[3] letters[4] letters[5] letters[6] letters[7] letters[8] letters[9]
= = = = = = = = = =
'a'; 'b'; 'c'; 'd'; 'e'; 'f'; 'g'; 'h'; 'i'; 'j';
// Aquí el índice es 0 // Aquí el índice es 1 // ... //
// Aquí el índice es 9
Pero así no tiene gracia utilizar arrays, ¿verdad? 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;
Inicialización de arrays Los elementos de un array se pueden inicializar junto con su declaración. Para ello se le asigna una lista ordenada de valores encerrados por llaves y separados por comas. Por supuesto, los valores deben ser compatibles con el tipo de dato del array. Este tipo de inicialización solo está permitido en la declaración del array. Ejemplos: unsigned char mask[3] = { 0xF0, 0x0F, 0x3C }; // Ok int a[5] = { 20, 56, 87, -58, 5000 }; // Ok char vocals[5] = { 'a', 'e', 'i', 'o', 'u' }; // Ok int c[4] = { 5, 6, 0, -5, 0, 4 }; // Error, demasiados inicializadores
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á precalculado 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: int a[] = { 70, 1, 51 }; // Un array de 3 elementos char vocals[] = { 'a', 'e', 'i', 'o', 'u' }; // Un array de 5 elementos char msg[] = "Este es un array de caracteres"; // Un array of 31 elementos
¿Por qué el último array tiene 31 elementos si solo se ven 30 letras? Lo sabremos luego.
Cadenas de texto terminadas en nulo
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: char Greet[10] = "Hello"; char msg[] = "Hello";
// 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 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
Visto gráficamente, msg tendría la siguiente representación:
Estructura de una cadena de texto.
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 ;).
Declaración de punteros 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é? La forma general de declarar un puntero es la siguiente: data_type * PointerName;
Los siguientes ejemplos muestran lo fácil que es familiarizarse con la declaración de los punteros: int * ip;
// ip es un puntero a variable de tipo int
char * ucp; unsigned char * ucp; const long * clp; float * p1, *p2;
// // // //
cp es un puntero a variable de tipo char Puntero a variable de tipo unsigned char Puntero a constante de tipo long Declara dos punteros a variable de tipo float
Apuntando a variables Decimos que una variable puntero “apunta” a una variable x si contiene la dirección de dicha variable. Para ello se utiliza el operador &, el cual extrae la dirección de la variable a la que acompaña. Un puntero siempre debería apuntar a una variable cuyo tipo coincida con el tipo del puntero. 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. void main (void) { int height, width; char a, b, c; float max; int * ip; char * cp; float * fp;
// ip es un puntero a variable tipo int // cp es un puntero a variable tipo char // Puntero a variable tipo float
ip = &height; ip = &width;
// Con esto ip tendrá la dirección de height // Ahora ip apunta a width
cp = &a; cp = &c; cp = &a;
// cp apunta a a // Ahora cp apunta a c // Ahora cp apunta a a otra vez
fp = &max; fp = &height;
// fp apunta a max // Error! height no es una variable float
//... }
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; int * p, * q;
// Variables ordinarias // p y q son punteros a variables de tipo int
p = &height; *p = 10;
// p apunta a height // Esto es como height = 10
p = &width; *p = 50;
// p apunta a width // Esto es como width = 50
height = *p;
// Esto es como height = width
q = &height; n = (*p + *q)/2;
// q apunta a height // Esto es como n = (height + width)/2
//... }
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.
Punteros y arrays ¿Cómo 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 dirección de su primer elemento, a menos que se especifique otro elemento. 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: void main (void) { int * p; // Declara p como puntero a int int n; // Alguna variable int mat[3] = { 78, 98, 26 }; // Array de variables int p = &mat;
// p apunta a mat (a su primer elemento)
n = *p; p++; n = *p; p++; n = *p;
// // // // //
*p = 10; p--; *p = 100;
// Con esto mat[3] valdrá 10 // Decrementar p para apuntar a elemento anterior // Con esto mat[2] valdrá 100
p = mat;
// p apunta a mat. Es lo mismo que p = &mat
p = NULL; // ...
// Desasignar p. Lo mismo que p = 0x0000
Esto da n = Incrementar Esto da n = Incrementar Esto da n =
78 p para apuntar a siguiente elemento 98 p para apuntar a siguiente elemento 26
}
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; a = str1[0]; a = str1[3];
// Esto da a = 'A' // Esto da a = 'a'
a = str2[0]; a = str2[3];
// Esto da a = 'P' // Esto da a = 'n'
str1 += 2; str2 += 2;
// Error! Str1 es estático // Correcto. Ahora str2 apunta a 'i'
str1++; str2++;
// Error otra vez! Str1 es estático // Correcto. Ahora str2 apunta a 'n'
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 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. 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. La sintaxis de los punteros en el encabezado de la función no es nada nuevo, teniendo en cuenta que también tienen la forma de declaraciones de variables. En el siguiente ejemplo la funcion interchage 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. void interchange( int * p1, int * p2 ) { int tmp = *p1; // Guardar valor inicial de variable apuntada por p1. *p1 = *p2; // Pasar valor de variable apuntada por p2 a... // variable apuntada por p1. *p2 = tmp; // Variable apuntada por p2 valdrá tmp. } void main (void) { int i, j; /* Hacer algunas asignaciones */ i = 10; j = 15;
/* Llamar a función interchange pasando las direcciones de i y j */ interchange( &i, &j ); // En este punto i vale 15 y j vale 10 // ... }
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 estarará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. float prom ( int * p, int size ) { int i; float tmp = 0; for ( i=0; i
// 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 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. void mayus( char * p ) { while( *p ) // Mientras carácter apuntado sea diferente de 0x00 { if( ( *p >= 'a' ) && ( *p <= 'z' ) ) // Si carácter apuntado es
*p = *p - 32; p++;
// minúscula // Hacerlo mayúscula // Incrementar p para apuntar sig. carácter
} } int lon( char * p) { int i = 0; // Declarar variable i e iniciarla a 0. while( *p ) // Mientras carácter apuntado sea diferente de 0x00 { i++; // Incrementar contador. p++; // Incrementar p para apuntar sig. carácter } return i; // Retornar i } void main (void) { int L; char song1[20] = "Dark Blue"; char song2[20] = "Staring Problem"; char song3[20] = "Ex-Girlfriend"; /* Obtener longitudes de los arrays de L = lon(song1); // Debería dar L = L = lon(song2); // Debería dar L = L = lon(song3); // Debería dar L =
texto */ 9 15 13
/* Convertir cadenas en mayúsculas */ mayus(song1 ); // Es lo mismo que mayus(&song1); // Ahora song1 debería valer "DARK BLUE" mayus(song2 ); // Es lo mismo que mayus(&song2); // Ahora song2 debería valer "STARING PROBLEM" mayus(song3 ); // Es lo mismo que mayus(&song3); // Ahora song3 debería valer "EX-GIRLFRIEND" 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 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 ascci 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).
Arrays constantes No es que me haya atrazado 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. Por ejemplo: const int a[5] = { 20, 56, 87, -58, 5000 }; // Array constante const char vocals[5] = { 'a', 'e', 'i', 'o', 'u' }; // Array constante const char text[] = "Este es un array constante de caracteres";
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. Es como si estuviéramos frente a una tabla hecha en ensamblador (de PICs) a base de instrucciones retlw. De hecho, los compiladores Mikro C, CCS C, Hi-tech C e IAR C, construirán internamente tablas semejantes para representar estos arrays. Si estos arrays constantes van a ser leídos directamente y mientras se utilice la notación de los corchetes para especificar a cada elemento, todo estará ok. Por otro lado, cada compilador trabaja diferente el paso de arrays constantes a funciones y su compatibilidad con los punteros. Por ejemplo, Hi-tech C soporta magistralmente estos temas. Por otro lado, CCS C no acepta nada, pero ofrece excelentes métodos alternativos para realizar esas tareas. De modo que será necesario revisar el manual de cada compilador en particular. En otros compiladores, como MPLAB C18 o BoostC, debe además añadirse el calificador rom (no perteneciente al C estándar). BoostC solo soporta arrays constantes con datos de 8 bits (de tipo char y compatibles) y su peculiar forma de declarlas es la siguiente. rom char * a = "cadena de texto constante en BoostC"; rom char * mat = { 20, 56, 87, -58, 50 }; // Array constante rom unsigned char * dec = { 10, 20, 30, 40, 50 }; // Array constante
Por lo demás, el acceso a los elementos de estos arrays tiene que seguir siendo mediante índices y corchetes. En el compilador MPLAB C18, la palabra rom va junto al const. Por supuesto que en los PIC18 los arrays en ROM se implementan con más eficiencia sin recurrir a los limitados retlw. rom rom rom rom
const const const const
char * a = "cadena de texto constante en MPLAB C18"; char a[] = "otra cadena de texto constante en MPLAB C18"; int mat[5] = { 20, 56, 87, -58, 5000 }; // Array constante long pots[] = {10, 100, 1000, 10000, 100000}; // Array constante
Proteus VSM Qué es Proteus VSM A lo largo de este curso no muestro interés personal para promocionar algún producto en particular. Pero “al César lo que es del César” y no tengo mayor reparo en asegurar que Proteus VSM es el mejor software de su clase que existe para aficionados y profesionales dedicados al desarrollo de proyectos con microcontroladores. Proteus ha revolucionado el concepto de simulación de circuitos electrónicos al pasar de las típicas presentaciones gráficas y textuales de información pre-procesada (al estilo PSPICE) a la simulación interactiva muchas veces en tiempo real. Podría seguir listando las potentes características de Proteus, pero como tampoco pretendo desafiar sus archivos de ayuda y manuales oficiales, los cuales son muy buenos y de lectura recomendada (para quien tenga tiempo:), prefiero aconsejarte que le eches un vistazo a los excelentes ejemplos que trae incluido. Quedarás más que asombrado.
Instalación de Proteus VSM El proceso de instalación no difiere mucho de cualquier otro software de Windows. Solo tenemos que pasar de Next en Next aceptando preferentemente todas las opciones que se nos ofrecen por defecto.
Instalación de Proteus. Terminada la instalación podemos ir al menú iniciar de Windows y ver todos los componentes disponibles incluyendo sus respectivos archivos de ayuda. La aplicación principal de Proteus se llama ISIS, tanto que el asistente de instalación colocó su acceso directo en el escritorio sin ni siquiera preguntárnoslo.
Componentes instalados de Proteus. Vemos que también está presente el programa ARES, el cual sirve para crear PCBs de circuitos, como Eagle o Protel. Es raro arrancar ARES para trabajar con él desde cero. Por eso no tiene acceso directo en el escritorio. Lo normal es dibujar el esquema del circuito en el entorno de ISIS y luego llevarlo a ARES.
Los Drivers de USB de Proteus Otro componente que nos podrá ser de mucha importancia son los drivers de USB. Proteus utiliza estos drivers para simular los microcontroladores con USB. Podemos ver en la figura de arriba que están disponibles el instalador y des-instalador de estos drivers. De hecho aún no están instalados. Podríamos hacerlo ahora dándole doble clic a “Install USB Drivers” pero la misma gente de Labcenter Electronics nos sugiere pensarlo dos veces antes de hacerlo. ¿Por qué? Además, está el hecho de que esos drivers de USB solo corren en sistemas operativos de 32 bits (al menos hasta el momento de actualizar este contenido [Octubre de 2011]). Si tratáramos de instalarlos en un SO de 64 bits nos saldría la siguiente ventanita.
Hablar de USB y Proteus es todo un tema aparte. Así que lo dejaremos para más adelante. Allí veremos cómo programar microcontroladores con USB y cómo simularlos en Proteus, incluso en Windows 7 de 64 bits.
Instalación de VSM Studio VSM Studio es un IDE (Entorno de Desarrollo integrado) desarrollado por Labcenter Electronics como plataforma alternativa para el desarrollo de proyectos con microcontroladores usando diferentes compiladores o ensambladores. No es un IDE más, como CodeBlocks o Eclipse, que son entornos genéricos y por ende presentan algunas falencias cuando se busca una configuración más personalizada. VSM Studio fue pensado exclusivamente para el trabajo con los compiladores y ensambladores de microcontroladores. Algunos de los compiladores que soporta VSM Studio son:
Para los PIC: CCS C (no incluye dsPIC), MPLAB C18, MPLAB C30, MPASM, Hi-Tech C para los PIC10/12/16, Hi-Tech C para los PIC18, Hi-Tech C para los dsPIC. Para los AVR: solo WinAVR y el ensamblador AVRASM. Para los 8051: IAR para 8051, Keil uVision4 para 8051 y el ensamblador ASEM-51. Para los ARM: IAR para ARM y GCC para ARM.
Se ve interesante, sobre todo si consideramos que VSM Studio se distribuye gratuitamente. No obstante VSM Studio está lejos de igualar al MPLAB para los PIC y más lejos del Studio 5 para los AVR. Ellos también son IDEs flexibles y se conectan con muchos más compiladores y con mejor soporte, además del hecho de que también pueden interactuar con Proteus gracias a los Pluggins respectivos. Quizá muchas veces nos pueda atraer la extrema ligereza de VSM Studio, cuando nos cansemos de lo que demoran en cargarse MPLAB o Studio 5. VSM Studio solo pesa cerca de 15 Mb, así que nada perdemos con descargarlo, instalarlo y probarlo. Su instalación no tiene nada de extraordinario. Conviene siempre visitar la web Labcenter Electronics para obtener las versiones más recientes de ésta y otras extensiones de Proteus.
Instalación de VSM Studio.
El Entorno de ISIS Proteus
Entorno de trabajo de ISIS. ISIS es el entorno de trabajo de Proteus. Aquí se diseñan, dibujan y simulan los circuitos. Bueno, no voy a describir cada uno de los elementos de esta ventana. Tú mismo puedes ir conociéndolos con tan solo señalarlos con el puntero del mouse. Además de las clásicas sugerencias que suele ofrecer como cualquier otro buen programa de Windows, Proteus muestra una pequeña descripción del elemento apuntado cuando se pulsa la tecla F1. Por ejemplo, la siguiente figura muestra la información contextual del botón Generator. Claro que está en inglés, pero igual se entiende aunque sea un poquito. Además, no me digas que no sabes lo que significan esas frasecitas como Copy to clipboard, Save current design, Zoom in, Zoom out, etc. ¡Por favor! En cuanto a los elementos y comandos que son más propios de Proteus, aprenderemos a usarlos de a poco. Esta toma corresponde a Proteus en Windows XP porque en Windows 7 esta característica no me funcionó ;)
Información interactiva de los elementos de ISIS.
Habilitar Gráficos de Open GL Es probable que al abrir ISIS proteus por primera vez nos aparezca el siguiente mensaje:
Mensaje de soporte de Open GL graphics. Allí nos dice que nuestra tarjeta gráfica puede soportar los gráficos Open GL a nivel hardware. Con Open GL Proteus producirá mejores resultados visuales tanto en el diseño del circuito como en las animaciones. Las simulaciones también serán más rápidas y fluidas, o mejor dicho, consumirán menos ciclos de CPU. Esto último es muchísimo más importante que el aspecto visual, por ejempo, recuerdo que la simulación de un letrero electrónico como el del programa ledsign, que consumía el 90% del CPU, se aceleraba y reducía el consumo de CPU al 60% con el soporte hardware de Open GL gráfico habilitado. Podemos marcar la casilla “Don‟t display this message again” para que no nos vuelva a mostrar esta ventana. Y si queremos habilitar Open GL para Proteus y obtener las mejora visuales indicadas vamos al menú System y escogemos Set Display Options… En la ventana que se nos presenta seleccionamos Use Open GL Graphics (Hardware Accelerated).
Habilitación de Open GL graphics para ISIS. Cuando cerremos esta ventana aceptando el cambio realizado nos aparecerá otra diciendo que nos aseguremos de actualizar los drivers de la tarjeta gráfica en caso de tener problemas con el display o el programa. Si los problemas persisten podemos revertir la configuración hecha. Podremos apreciar algunos efectos de Open GL en las siguientes capturas de pantalla, por ejemplo, cuando un componente apuntado se resalta con un rectángulo relleno en vez de un rectángulo de contorno punteado.
Open GL habilitado Open GL sin habilitar
Creación del Diseño
Un diseño está compuesto por diversos objetos, desde los cables de interconexión hasta los instrumentos virtuales como el osciloscopio o el terminal serial. Los objetos están agrupados según su categoría con sus respectivos iconos en la barra de herramientas izquierda de ISIS. A continuación citamos algunas categorías, llamadas modos: Components. Representan todos los componentes electrónicos presentes en un circuito, como los LEDs, condensadores, microcontroladores, etc. Virtual Instruments. Ofrece los instrumentos de medición como el Osciloscopio, voltímetro, frecuencímetro, etc. Generator, para producir ondas cuadradas, sinusoidales, de reloj, etc. Simulation Graphs, para visualizar los resultados de la simulación en gráficos, al estilo de los programas como Electronics Workbench o Cadence Orcad. Terminals. Nos permitirá colocar los terminales de entrada, salida, así como tierra GND y las fuentes de alimentación VCC o VDD. Los terminales de entrada INPUT y salida OUTPUT son muy útiles para conectar elementos sin necesidad de unirlos directamente con cables. Etc. Etc.
Colocando los Componentes El panel izquierdo de ISIS (que en la figura de abajo se ve con el título de TERMINALS) se llama Object Selector y despliega los objetos de la categoría seleccionada actualmente (Components, Generator, etc.). Para añadir un objeto a la hoja de diseño primero debemos seleccionar su categoría y luego sacar del Object Selector el elemento deseado. Por ejemplo, para colocar los símbolos de tierra y alimentación seguimos los tres pasos mostrados en la siguiente figura.
Colocando objetos en la hoja de diseño. Para colocar los componentes electrónicos como resistencias o diodos aparece un paso intermedio que es llenar el Object Selector con los dispositivos deseados. Para ello hacemos clic en el icono Components
de
la barra de herramientas y luego en el botoncito P. Un modo rápido es clicar el icono de la barra de herramientas superior o sencillamente presionar la tecla “P”. En cualquier caso nos veremos con la ventana Pick Devices.
Sacando dispositivos de la ventana Pick Devices. En la caja de texto Keywords se escribe el componente buscado. Nota que Proteus tiene un excelente motor de búsqueda y no es necesario escribir el nombre completo de una parte ni usar comodines como en otros programas. A medida que se va escribiendo, Proteus va mostrando todos los elementos coincidentes.
Una vez localizado el componente buscado, le damos doble clic para enviarlo al Object Selector. Así podemos ir sacando los dispositivos restantes. Cuando hayamos terminado con esta parte cerramos esta ventana dándole al botón Ok, Cancel o, mejor aún, presionando Escape en el teclado. Ahora el panel Object Selector contiene los diversos dispositivos que formarán nuestro circuito. Selecciónalos uno a uno y luego colócalos en el esquemático con un clic izquierdo.
Colocación de elementos en la hoja de diseño. En este punto vamos a detenernos a comentar algunos componentes que resultarán muy frecuentes en el futuro.
ATmegaNN4P o ATmegaNN8P. Puede que en algunos casos el AVR que veas en mis esquemas no se parece al que tienes en tu Proteus. Lo que importa es que sea o tenga el mismo modelo, el resto es solo cuestión de apariencia LED-BLUE, LED-GREEN,... Hay varios tipos de diodos LED disponibles. Para las simulaciones se suelen usar los LED animados (los que prenden). Puesto que son solo simulaciones, los colores quedan en segundo plano. RESISTOR. Al escribir resistor en la casilla Keywords se listarán muchísimas partes, desde los modelos genéricos hasta los resistores comerciales. En principio, podríamos elegir cualquiera de ellas; de todos modos, la impedancia puede reajustarse fácilmente. También, se podría afinar la búsqueda añadiendo el valor de la resistencia. BUTTON. Así se llaman los infaltables pulsadores.
Mover, Rotar y Reflejar los Objetos Como los objetos no siempre se presentarán en la posición deseada, conozcamos algunas formas de cambiarlas. Un objeto queda seleccionado por un clic izquierdo del mouse. Con ello aparecerá resaltado en color rojo (por defecto). Se quita la selección de un objeto haciendo otro clic sobre alguna parte vacía de la hoja de diseño. Para editar la posición de un elemento selecciónalo y abre su menú contextual (de clic derecho). Allí encontrarás las diversas opciones disponibles, como mover, borrar, rotar en un sentido u otro, o reflejar (Mirror) sobre el eje X o Y.
Menú contextual de un objeto en ISIS. Alternativamente, puedes usar otros atajos, por ejemplo:
Para mover un objeto, selecciónalo y luego arrástralo con el botón izquierdo hasta el lugar deseado.
Para rotar un objeto, selecciónalo y luego presiona la tecla „+‟ o „-‟ del teclado numérico tantas veces hasta conseguir la orientación deseada. Otra forma de hacerlo es aplicando los botones de rotación al objeto seleccionado. La rotación solo es posible en ángulos rectos.
También se pueden usar los botones de rotación y reflexión mostrados abajo, aunque ellos tendrán efecto solo en los elementos del Object Selector, o sea, antes de colocarlos en la hoja de diseño.
Botones de rotación y reflexión.
Para eliminar un objeto, dale doble clic derecho, o selecciónalo y usa la tecla supr.
La selección de un grupo de objetos se logra encerrándolos en un rectángulo dibujado con el botón izquierdo. Una vez hecho esto el menú contextual del botón derecho desplegará las opciones de edición de grupo como copiar, mover o rotar/reflejar. Estas opciones también están presentes en la barra de herramientas. Entre todo, la forma más fácil de mover un grupo de objetos seleccionado es arrastrarlos con el botón izquierdo.
Botones para editar grupos de objetos. En la siguiente figura se aprecia el copiado múltiple de un grupo de objetos.
Copiado múltiple de un grupo de objetos en ISIS. Con frecuencia tras la edición de los objetos aún quedarán restos o marcas de dibujo sobre el esquemático. Para refrescar de forma rápida todo el diagrama se puede presionar la tecla R (de Redibujar).
Interconexión de Componentes Los cables para la interconexión de los elementos del circuito no están en una categoría en especial. Se colocan directamente, aprovechando la forma de lápiz del cursor y las marcas de snap (esos pequeños cuadraditos □) que aparecen al acercar el lápiz a algún terminal conectable.
Interconexión del circuito.
Componentes Innecesarios Para simplificar la tarea de simulación Proteus permite obviar algunos elementos en el diseño. Pines GND y VDD. Tal vez te inquiete la ausencia de los pines GND y VDD de los dispositivos digitales como los microcontroladores, pero eso no cuenta para la simulación porque Proteus asume que trabajarán con alimentación de 0 y 5 V. El circuito oscilador del microcontrolador. Los microcontroladores pueden trabajar con varios tipos de osciladores siendo el más común el conformado por un cristal de cuarzo y dos capacitores de estabilización. Sin embargo, debes saber que para los microcontroladores este circuito no tiene directa intervención en la simulación. La frecuencia de operación del microcontrolador se establece en su ventana de propiedades, con lo cual el circuito del XTAL quedaría de adorno. Muchas veces se lo suele ignorar para aligerar la carga del diseño y mejorar en algo la velocidad de simulación.
Circuito oscilador típico de un microcontrolador. Los capacitores de desacoplo. Ya que en el entorno ideal de ISIS no hay ruido ni interferencias de ese tipo, no hacen falta los capacitores que en un circuito real se colocan en los pines de alimentación VCC o VDD de los circuitos integrados.
Simulación del Diseño En cualquier momento, inclusive con el diseño a medio armar, ya se puede correr la simulación para ver cómo va quedando y en el camino ir añadiendo, quitando o modificando componentes. Solo hay que jugar con los botones Play y Stop mostrados abajo. Los otros botones centrales son para pausas aunque es en el modo debugging del microcontrolador donde se les saca verdadero provecho.
Botones de control de la simulación. Sin embargo, si hay al menos un microcontrolador presente en el circuito, la simulación no arrancará sino hasta haber cargado su firmware. Este tema es muy crucial así que se le dedica toda una sección más adelante.
Edición de las Propiedades de los Objetos Una de las grandes ventajas de las simulaciones es que se pueden cambiar los elementos del diseño o sus valores con suma facilidad.
Por lo general y como se puede comprobar, muchos de los componentes de Proteus no requieren nada de edición y, de hecho, se pueden encontrar partes con modelos que ya tienen todos los parámetros correctamente calibrados. No obstante, muchas veces será más cómodo cambiar alguna propiedad de un dispositivo. Para editar las propiedades de un objeto hay que darle doble clic izquierdo. Así se accede a su ventana de propiedades, la cual, por supuesto, variará de acuerdo con el objeto tratado. Por ejemplo, si quisiéramos cambiar el valor de una resistencia, solo habría que modificar el parámetro Resistance en su ventana Edit Component.
Ventana de propiedades de una resistencia. La fuente de alimentación POWER tiene un valor de 5 V por defecto, aunque no se note así en el esquemático. Si quieres ver que al menos diga VDD o VCC, puedes abrir su ventana de propiedades y escoger una opción del combobox.
Ventana de propiedades de un terminal de alimentación. Claro que el susodicho combobox no ofrece tensiones ordinarias. Si las requiriéramos las tendríamos que escribir a mano. Por ejemplo, para una fuente de 12V se escribe +12V al lado de String. La V no hace falta, pero el signo al inicio es imprescindible. Además, no se deben dejar espacios en blanco.
Cargar el Programa del Microcontrolador La configuración del microcontrolador puede pasar por cambiar muchísimos parámetros. En este momento solo veremos los dos que realmente interesan en la gran mayoría de diseños. El programa del microcontrolador y su frecuencia de operación. Muy bien, ahora dale doble clic al microcontrolador de tu circuito para abrir su ventana de propiedades. Obviamente será una ventana diferente para cada tipo de microcontrolador, pero empezaremos por notar que todas tienen en común al menos la propiedad Program File. Como ejemplo se resalta a continuación dicho campo para los microcontroladores PIC18F4550, ATmega324P y ARM LPC2114.
Ventana de propiedades del PIC18F4550.
Ventana de propiedades del ATmega324P.
Ventana de propiedades del ARM LPC2114. Cualquiera que sea el microcontrolador, al hacer clic en el punto indicado, se abrirá el cuadro de diálogo Select File Name. Solo queda buscar y seleccionar el archivo de programa del microcontrolador, el cual puede tener diferentes formatos. La siguiente ventana corresponde al cargador de un AVR. Se nota que al menos en el combobox Tipo aparecen como aceptables las extensiones HEX, COF, ELF, OBJ o de tipo UBROF, dependiendo del compilador con que se trabaje. En otros casos como para los PIC este filtro indica poco o nada.
Cuadro de diálogo para seleccionar el programa del microcontrolador. Si no estás seguro/a de qué tipo de archivo genera tu compilador o cuál debes elegir, a continuación doy un panorama sobre los más comunes.
El Archivo HEX Es el mismo que se utiliza para grabar el microcontrolador. Todos los compiladores o ensambladores generan un archivo HEX al final, aunque su formato puede variar entre Intel hex o Motorola hex, generalmente. Con este archivo la simulación en Proteus solo será fluida (como si fuera poco) y servirá de nada si hacemos pausas para ver por dónde anda la ejecución del programa del microcontrolador.
El Archivo COFF COFF (Code Object File Format) es el archivo de depuración más extendido y lo utilizan casi todos los compiladores para PICs y algunos para AVR. Si en siguiente lista encuentras tu compilador, entonces sabrás que debes buscar el archivo con extensión .COF para cargarlo en tu microcontrolador PIC o AVR. Los compiladores CCS C para PICs y dsPICs. Los compiladores de Hitech C para PICs y dsPICs.
Los compiladores MPLAB C18 y C30 para los PIC18F en adelante. Los compiladores Mikro C para PICs y PIC32. Los compiladores SDCC, BoostC y otros. Incluso el ensamblador del MPLAB usa el archivo COFF para sus PICs. También hay compiladores para AVR que generan un archivo de depuración COFF, por ejemplo. El compilador CodeVisionAVR para los AVR desde los tinys hasta los ATxMegas. CodeVisionAVR no soporta los AVR32. El compilador ImageCraft C también para los AVR desde los tinys hasta los ATxMegas. ImageCraft C tampoco soporta los AVR32, aunque sí tiene compiladores para otros microcontroladores como los CPU12 de Freescale, los PSOC1 de Cypress o los MSP430 de Texas Instruments. Los compiladores Mikro C para los AVR.
El Archivo ELF Técnicamente la estructura de los archivos ELF provee mayor información de depuración que sus similares y es el más recomendado por Proteus para sus simulaciones. No obstante ello, son pocos los compiladores que lo promueven. Los compiladores que sobresalen trabajando con los cargadores ELF son GCC AVR para todos los AVR incluyendo los ATxMega, su compilador hermano GCC AVR32 para los AVR32 y su compilador primo hermano GCC ARM para los microcontroladores ARM. Puesto que hay otras herramientas de simulación aparte de Proteus que trabajan preferentemente con los archivos COF, estaba en desarrollo una utilidad que convierte los archivos ELF en archivos COFF o archivos COFF Extendido de Atmel. Pero la migración de Atmel al formato ELF desincentivó el proyecto, que actualmente aún está disponible, aunque con algunos bugs. Vale la pena saber que también con el compilador Hitech C se pueden obtener archivos de depuración ELF entre algunos otros formatos.
Los Archivos UBROF Este formato es exclusivo de los compiladores IAR C de la empresa IAR Systems. En las prácticas con Proteus son los archivos que brindan la mejor información de depuración. UBROF más bien hace referencia a la información que lleva el archivo, la extensión que en realidad debes buscar es otra, por ejemplo D90. Los compiladores de IAR Systems se distinguen de los demás en que no generan este archivo para Proteus de forma automática. Es necesario modificar la configuración predeterminada. Para ver cómo hacerlo puedes ir a esta sección.
Los Archivos BAS y OBJ
Debemos hacer una observación especial en los compiladores Basic. O mejor, simplificaré las cosas diciendo que si trabajas con el compilador para PICs Proton Plus, de Crownhill Associates, es el mismo archivo BAS del código fuente el que debes cargar para tu simulación. Esta tarea es más sencilla porque se puede realizar directamente desde el mismo entorno de Proton Plus. Pero atención, si trabajas con el compilador Basic Bascom AVR, el archivo que debes cargar es el que tiene extensión OBJ. Esta carga es manual. El archivo OBJ no es exclusivo para simular un programa compilado con Bascom AVR. También los proyectos en ensamblador con Studio 5 dan como resultado ese archivo y se cargan del mismo modo.
Ventana de un PIC18F con su archivo COFF cargado.
Simulación del Microcontrolador Imagino la ansiedad por simular el microcontrolador de quienes vienen leyendo el tutorial desde el principio. Recordemos que Proteus puede simular incluso los circuitos a medio armar, pero ninguna simulación arrancará si tiene un microcontrolador sin su programa cargado. Como en este punto ya cumplimos las condiciones mínimas, podemos ver los resultados de los siguientes botones de simulación.
Botones de control de la simulación.
Simulación de la práctica simpleseq. ¡Simulación en tiempo real! Qué diferencia con otros simuladores, ¿verdad? Sin embargo, que la simulación sea la que esperábamos o no es otra cosa. Editar componentes como resistencias o condensadores es muy sencillo, y eso, si es que hace falta. Lo que quiero decir es que si la simulación es defectuosa lo más probable es que el problema esté en el microcontrolador. Tal vez no ajustamos bien la frecuencia del procesador o quizá pusimos mal algún fuse del microcontrolador.
Configuración de los Fuses del AVR Puede que cada tipo de microcontrolador tenga un grupo de fuses diferente. Lo bueno del caso es que muchos de los fuses tienen poca relevancia y muchos otros son conocidos y comunes. Por ejemplo, sin importar de qué marca sean, casi todos los microcontroladores tienen fuses de configuración del oscilador o de habilitación del Watchdog Por otro lado, Proteus presenta inicialmente los microcontroladores con los fuses con su configuración por defecto o de fábrica, la cual suele ser suficiente.
Bueno, para ir directo al grano en la siguiente figura se muestra la configuración de los fuses relacionados con el oscilador del sistema.
Selección del oscilador de 8 MHz para un microcontrolador AVR. El fuse CLKDIV8 viene programado por defecto, de modo que el valor inicial del prescaler del oscilador principal del AVR será 8, y como resultado veremos una simulación 8 veces más lenta de lo que establece la fuente del oscilador. ¿Y cuál es ésa? Antes de responder debo aclarar que el valor del mencionado prescaler se puede modificar por software. En ese caso no importará el valor de CLKDIV8 porque el mismo programa del AVR calibrará su prescaler en tiempo de ejecución. Pero como ése no es un método muy usado, lo más recomendable será poner este fuse en Unprogrammed (no programado), como se indica arriba. Ahora bien, la fuente del oscilador del sistema puede ser interna o externa y se define por los fuses CKSEL. En casi todas las prácticas de cursomicros.com se utiliza un circuito externo con XTAL de 8MHz. Sin embargo, para las simulaciones con Proteus, ya sea por practicidad o pereza, dará lo mismo (de hecho yo lo hago) si utilizamos el oscilador interno RC que también brinda 8 MHz, por defecto. Esto es solo para la simulación porque allí el oscilador RC es tan estable como el XTAL.
Para quienes no quieran entrar en rollos o para quienes deseen simulaciones a frecuencias diferentes de 8MHz pueden escoger cualquiera de las opciones de los fuses CKSEL que indica XTAL externo y escribir la frecuencia del XTAL usado en la categoría Clock Frecuency.
Selección de un oscilador de frecuencia personalizada para un AVR. Me parece que con la configuración de fuses vista bastará para las simulaciones con AVR en Proteus. Lo aquí tratado es aplicable a todos los AVR de las familias mejoradas, como los ATmegaxx4y, ATmegaxx8y, ATmegaxx5y ATmegaxx9y, entre otros. En ciertos modelos pueden aparecer o desaparecer algunos fuses, pero su efecto suele ser secundario.
Configuración de las Propiedades del PIC También en los PICmicro los principales parámetros a configurar están relacionados con los osciladores.
En los PICmicro la frecuencia de operación del procesador no es la que marca su fuente de reloj, sino que internamente se divide por 4. Si, por ejemplo, un PIC16F trabaja con un XTAL de 8MHz, entonces su procesador operará a 2MHz. En este caso para la simulación en Proteus lo que cuenta es la frecuencia del XTAL, aunque la ventana de propiedades indique Processor Clock Frequency. Adicionalmente los PIC18F (y superiores) pueden multiplicar la frecuencia de su fuente de reloj para conseguir una velocidad de operación mayor.
Selección de un oscilador para un PICmicro. Los modelos de los PICmicro de Proteus son tan completos que a veces, más que ayudar, pueden derivar en una mella para la performance de la simulación. A continuación veremos algunas de las propiedades del PIC cuya configuración sería más conveniente que la hiciera el usuario directamente si así lo demandase.
Palabra de Configuración y Propiedades avanzadas del PIC16F877A. Program Configuration Word: La palabra de configuración contiene los Bits de Configuración. Aunque no se reflejen directamente, sus valores se cargan desde el mismo archivo HEX, por lo que este campo no debería quitarnos la atención. Aun así, en Proteus hace falta algo más para que los bits de configuración entren en la simulación. Sigo hablando de esto en lo subsiguiente. Randomize Program Memory: Raramente el programa del PIC alcanzará el límite de la memoria de programa. Las posiciones restantes normalmente quedan como datos 3FFF y así se cargarán para la simulación. Si por algún extraño motivo quisieras que esas posiciones se rellenaran con valores aleatorios, podrías establecer esta propiedad con un YES. Randomize Data Memory: Igual que el caso anterior pero con la memoria de datos. Generalmente las variables utilizadas en un programa deberían estar inicializadas. Así que esta propiedad tampoco debería interesar. Model PIC Start-up Delays: Se refiere al retardo inicial producido principalmente por el temporizador Powerup Timer. Es un tiempo que dura 72 ms tras conectarse la alimentación del PIC. Durante este lapso el PIC se mantiene en estado de RESET (para que la alimentación del circuito se estabilice) y luego recién ejecutará la primera instrucción del programa. Es el mismo tiempo que se habilita con el fuse _PWRTE_ON. En conclusión, si queremos simular el tiempo del Power-up Timer en Proteus, además de activar el fuse en el código del programa, debemos poner un YES en este campo. Model PIC Wake-up Delays: Se trata de un tiempo de 1024 ciclos de reloj (256 µs para un XTAL de 4MHz) generado por el temporizador interno Start-up Timer. También complementa al Power-up Timer pero sirve básicamente para esperar que el reloj del sistema se estabilice luego de salir del modo Sleep. En el chip real
este temporizador no depende de nosotros ya que siempre estará activado. Es solo un tema de Proteus y la verdad es que, como 1024 ciclos son poco apreciables, se suele ignorar para las simulaciones. Generate Q Clocks on CLKOUT Pin: Cuando el PIC opera con un oscilador RC externo (de frecuencia Fosc) en vez de un XTAL, el pin OSC2/CLKOUT sacará una onda cuadrada de frecuencia Fosc/4. Y nosotros sabemos que a Proteus le interesa un bledo si se usa XTAL, circuito RC u otro oscilador. Por tanto, poniendo YES en este campo el pin OSC2/CLKOUT mostrará la onda de Fosc/4 (siendo este Fosc la frecuencia configurada en la ventana de propiedades del PIC), independientemente del oscilador usado. Como sea, una señal de este orden en Proteus sería demasiado pesada de simular, por lo que es preferible dejarla tal como está, inhabilitada. Watchdog Timer Period: El Watchdog es un temporizador que tiene un tiempo base de 18ms, el cual varía ligeramente con la temperatura del chip. Al igual que todas las partes digitales de Proteus, el PIC no tiene un parámetro que interactúe directamente con la temperatura establecida para el diseño. Si se desea una simulación con un valor de Watchdog Timer Period un poquito diferente del típico, éste es el campo donde se cambia. Port Pin Low-High Delay y Port Pin High-Low Delay: En el mundo real los niveles de tensión en los pines del PIC no permutan instantáneamente. Estos tiempos de propagación son del orden de los nanosegundos y Proteus suele despreciarlos por defecto para la simulación. En caso de tener algún diseño donde ese parámetro realmente te importe debes indicar en este campo los tiempos pertinentes. (Los puedes hallar en el datasheet.) Data EEPROM Write Delay. Según su datasheet, los PIC16F tienen una EEPROM cuyos datos se graban en un tiempo típico de 4 ms, llegando a 10 ms como mucho. En este campo se puede establecer algún valor en particular. Initial contents of EEPROM. Es la misma EEPROM del punto anterior. Se supone que el contenido inicial de esta memoria también puede formar del archivo HEX; así que este campo será raramente cargado.
Depuración del Programa del AVR Con este modo podremos ver la ejecución del programa paso a paso, ver en diversas ventanas el cambio de los registros y demás datos de la RAM del microcontrolador, el contenido de la memoria EEPROM interna, la evolución de la Pila, etc. En general, digamos que es parecido al Depurador del Studio 5 pero muchísimo mejor, como lo comprobarás. Proteus no solo simula el microcontrolador, sino que lo hace en interacción con el resto de circuito. Una simulación paso a paso requiere que el microcontrolador tenga un archivo de depuración cargado. Como ya hablamos ampliamente de todo eso, vamos directamente a nuestro diseño de ISIS, presionamos el botón Step o la combinación Ctrl + F12 del teclado y ¡voilà!: tenemos la ventana AVR Source Code. Por cierto, las capturas presentadas en adelante corresponden al debugging del programa del LED parpadeante flashled.
El diseño en modo depuración.
Botones de depuración de la ventana Source Code. En la esquina superior derecha de esta ventana están los botones de depuración, con nombres ya familiares para nosotros: Sobra decir que es preferible acostumbrarse a las teclas de atajo que los identifican (las he puesto en azul). Play o Execute (F12). Ejecuta la simulación de corrido. Step over the current Function/Subroutine (F10). Como el Step over de cualquier otro simulador. Si la instrucción actual es una llamada a una función o subrutina, se ejecuta toda de golpe sin entrar en su interior. En el caso de los delays relativamente largos notaremos una breve desaparición de la ventana de código. Step into the current Function/Subroutine (F11). Tampoco nada nuevo. Si la actual instrucción es una llamada a subrutina o función, se entra en su interior. Step out of the current Function/Subroutine. Lo mismo de siempre. Si ya te cansaste de estar dentro de una subrutina o función, presiona aquí para salir de ella. Execute until the current cursor position is reached. Lo que dice su nombre: ejecutar el programa hasta alcanzar la posición actual del cursor. Este botón solo se activa si se apunta a una posición válida y diferente de la instrucción actual. Se suele usar para medir el tiempo empleado en ejecutarse el código entre dos puntos del programa. Toggle breakpoint at source line off->on->disabled (F9). Supongo que aún recuerdas qué son los breakpoints: puntos del programa donde se pausará la simulación. Con este botón los pones, quitas o inhabilitas. También se puede usar el doble clic.
Ventanas de Depuración Cuando se arranca la simulación, al final del menú Debug aparecen los ítems de varias ventanas de depuración (Simulation Log, Watch, etc.) que nos recuerdan al menú Debug Windows del Studio 5 cuando
se inicia el modo Debugging. Obviamente Proteus soporta mucho más que un microcontrolador y este menú podrá crecer según el diseño. Excepto Source Code y Variables, las demás ventanas siempre deberían estar disponibles, incluso para una depuración que solo use el archivo HEX.
Opciones del menú Debug en tiempo de depuración. En la siguiente toma se aprecian las ventanas de depuración relacionadas con el microcontrolador. El aspecto de todas ellas es fácilmente configurable mediante su menú contextual. Allí podrás cambiar colores, tipos de fuente, formatos numéricos de presentación, campos a visualizar (si es el caso), etc.
Las ventanas de depuración.
Simulation Log. Envía un reporte de los detalles de la simulación. En principio muestra las librerías de simulación cargadas, la versión del kernel de SPICE utilizada y el tamaño del archivo (HEX, COF u otro) cargado. Pero, más importante, también desplegará los errores, mensajes y advertencias de los eventos ocurridos durante la simulación, normalmente referidos al microcontrolador; por ejemplo, cortocircuitos (contention on net), acceso a un registro no implementado del microcontrolador, etc. Así que cuando sientas que algo anda mal con la simulación no dudes en revisar en primer lugar esta ventana.
AVR Source Code. La estuvimos viendo desde el principio. Si hay varios microcontroladores en el diseño, se presentará una ventana de código para cada uno. En realidad, eso pasará con todas las ventanas AVR.... Como se ve en la siguiente figura su menú de botón derecho ofrece varias opciones como configurar los colores, el tipo de fuente, etc. Si haces clic en Dissassembly, veras el código fuente con su equivalente en ensamblador. Es una vista muy práctica.
AVR Variables. Es para mí una de las más indispensables. Aquí se visualizan las variables que procesa el programa actualmente. Por ejemplo, en la siguiente figura está mostrando la variable n de la función delay_ms y el color rojo dice que su valor acaba de cambiar a 300. Yo no sé qué haría sin esta ventana ;). (Esta ventana corresponde al debugging del programa del LED parpadeante flashled.)
AVR CPU Registers. Muestra el registro de estado SREG, todos los Registros de Trabajo (R0 a R31) del AVR, incluyendo los punteros X, Y y Z. También muestra la siguiente instrucción a ejecutarse junto con el valor del Contador de Programa PC. La S que se ve al fondo representa el Puntero de Pila o Stack.
AVR Program Memory. Visualiza el código del programa en hexadecimal, que poco o nada se entiende, por eso raramente se le muestra.
AVR Data Memory. Aquí apreciaremos los valores de todos los datos de la RAM del AVR. Lo malo es que no es nada fácil distinguirlos. Si de examinar las variables del programa se trata, es preferible ver la ventana AVR Variables, ¿cierto? Bueno, tal vez sirva para descubrir con cierta facilidad cómo se van colocando los datos en la Pila, que también forma parte de la RAM. Una mejor alternativa puede ser la ventana Watch Window.
Watch Window. Es como la ventanita homónima del Studio 5. Allí podemos poner solo los datos que nos interesen, entre variables de la RAM, Registros de Trabajo, Registros de E/S, datos de la EEPROM interna e incluso de la misma memoria FLASH del AVR. Solo a modo de ejemplo en la siguiente figura he puesto en mi Watch Window los registros PORTC, PORTB y PIND. Fíjate en que el pin 0 de PORTB vale 1; debe ser que en ese momento el LED parpadeante está prendido.
AVR I/O Registers. Esta ventana muestra todos los registros de E/S del AVR, es decir, los registros que controlan los puertos, el USART, el conversor ADC, etc. Como ves, tampoco se puede distinguir mucho porque está en hexadecimal. Así que también en este caso es preferible optar por la ventana Watch Window, si es que el Registro de interés no es visualizado en la ventana AVR Variables.
AVR EEPROM Memory. Muestra el contenido de la memoria EEPROM interna del AVR.
Uso de la Ventana Watch Window Los archivos de depuración producidos por los compiladores de alto nivel despliegan en la ventana AVR Variables las variables y registros que el programa utiliza. Por desgracia, no todos los archivos de depuración ofrecen la información con el mismo grado de detalle. Los mejores en este sentido son sin duda los compiladores IAR C. De haber algunos datos que no se visualicen en AVR Variables una forma (no muy agradable) de resolver este impase es sacarlas manualmente a la ventana Watch Window. Éste es el menú contextual (del botón derecho) de la Watch Window.
Menú contextual de la ventana Watch. Si elegimos la opción Add Items (By Name)..., veremos una ventana con todos los Registros de E/S del AVR, los cuales podemos enviar uno a uno a la Watch Window dándoles doble clic.
Figura 1-36 Adición de registros especiales a la ventana Watch. En cambio, al tomar la opción Add Items (By Address)... estaremos frente a una ventana más completa donde además de los Registros de E/S podremos acceder a los Registros de Trabajo, a los datos de la RAM, de la EEPROM interna y de la misma memoria FLASH.
Adición de variables a la ventana Watch. ¿Sabes cuál es la diferencia entre AVR SRAM y AVR Data Memory en la figura mostrada? Haremos un ejercicio mientras respondemos a la pregunta. Teniendo en cuenta el programa en WinAVR del LED parpadeante flashled mostraremos en la Watch Window la variable n de la función delay_ms. Normalmente el archivo ELF de WinAVR solo muestra en la ventana AVR Variables los datos que se están en la memoria RAM y no los que se ubican en los Registros de Trabajo, que es donde precisamente está la n. Pero supongo que habrás notado que los Registros de Trabajo no aparecen en las opciones de Memory. Y he aquí la respuesta: en tanto que AVR Data Memory da acceso solo a los datos propios de la RAM (direcciones 0x100 en adelante, según el AVR), mediante AVR SRAM podemos acceder tanto a los Registros de Trabajo como a los Registros de E/S (que están entre las direcciones 0x00 y 0xFF) direccionándolos en modo de memoria de datos. Parece que la referencia estuviera al revés, ¿verdad? Éstas son cosas de Proteus más que de los AVR. Sea como fuere, lo que nos toca hacer es poner la configuración indicada en la siguiente figura y presionar Add para que la n vaya a la Watch Window.
Adición de variables a la ventana Watch. Ahora la explicación porque en cursomicros las cosas no se hacen así nada más. En Memory escogemos AVR SRAM, por lo arriba explicado, en Name escribimos un nombre cualquiera para la variable (en nuestro caso n) y en Address ponemos su ubicación, que son los Registros de Trabajo R24 y R25, los cuales vistos como parte de la RAM empiezan en la dirección 0x18. Las otras dos opciones se deducen del hecho de que n es una variable unsigned integer, de 2 bytes. La ubicación de n se observa en la vista dissassembly de la ventana Source Code. Para esto hay que tener cierto conocimiento del lenguaje C y del ensamblador de los AVR.
La ventana Source Code en modo Dissassembly.
Los Instrumentos Virtuales El icono Virtual Instruments de la barra de herramientas despliega en el Object Selector la lista disponible, de la cual en este momento solo veremos un par de ellas. No tendría sentido que explicásemos los depuradores SPI DEBUGGER e I2C DEBUGGER porque, imagino, aún no tenemos idea de lo que significan. Por otro lado, supongo que tampoco te hace falta leer un tutorial para aprender a usar los voltímetros y los amperímetros. Sobre los demás instrumentos, o son muy poco usados o son igual de sencillos.
Los instrumentos virtuales.
Uso del COUNTER TIMER Aunque la barra de estado de ISIS va mostrando el tiempo de simulación transcurrido, no es la mejor forma de medir los tiempos de ejecución de los códigos. El Timer Counter se puede configurar como cronómetro (Time), frecuencímetro (Frequency) o contador de pulsos (Count) en su ventana de propiedades, pero también se puede hacer al vuelo en plena simulación. De sus tres terminales normalmente el único que se conecta es el CLK. CE puesto a 0 detiene su operación y RST reinicia el instrumento.
Counter Timer y su ventana de propiedades. Cuando la simulación/depuración esté en ejecución dale clic izquierdo para que aparezca con ventanita más grande, como la siguiente.
Counter Timer “maximizado”. Observa los cuatro botones, que también presentes en su ventana de propiedades:
MODE establece la operación del Counter Timer como cronómetro (Time), frecuencímetro o contador de pulsos (Count). MANUAL RESET. Es más cómodo hacer el RESET aquí que desde el pin RST. RESET POLARITY indica si el RESET será en el flanco de subida o de bajada del botón anterior. GATE POLARITY establece si en modo contador el incremento será con los pulsos positivos o negativos.
Como práctica, veamos el tiempo exacto que toma una función delay_ms(), por ejemplo de nuestro programa del LED parpadeante flashled.
Luego de colocar un Counter Timer sobre el esquemático es conveniente regresar a la categoría Component (son “cosas de Proteus”). Iniciamos la simulación/depuración y clicamos el Counter Timer para agrandarlo y verlo mejor. Nos ubicamos en cualquiera de las líneas delay_ms() y reseteamos el Counter Timer.
Ejecutamos toda la sentencia delay_ms(500) con Step Over. Como 500ms es un poco grande veremos desaparecer la ventana de código por ese lapso, pero al final veremos que el cronómetro midió 500377 us, o sea, 500.377 ms.
Ahora mediremos el tiempo que transcurre entre las sentencias de las líneas 32 y 35 de este mismo código, solo que esta vez lo haremos empleando la opción Run To Source Line (ejecutar hasta la línea de código indicada).
Sea continuando con la simulación o reiniciándola, debemos seguir los tres pasos indicados en la siguiente figura.
En este punto el triangulito rojo debe señalar la línea 32 y el cronómetro debe indicar 00.000000, como en la siguiente figura. Ahora sigue los dos pasos que allí se indican.
Y… ¡sorpresa! (La sorpresa es para mí porque en verdad no esperaba dar este resultado como ejemplo. ;) Obtuvimos 500.377 ms otra vez. Si ahora se ha ejecutado más código, ¿cómo es que el cronómetro calculó el mismo tiempo?
Sucede que entre las líneas 32 y 35, además del delay que toma 500377us, están las sentencias PORTB |= 0X01 y PORTB &= ~0X01, las cuales se ejecutan en 0.5us, y el cronómetro no puede visualizar fracciones de microsegundos. O sea que la diferencia está solo que no se aprecia por ser muy pequeña. Lo que yo hice para medirla fue bajar la frecuencia del AVR a 1MHz.
Otra forma de medir tiempos de ejecución entre dos puntos es mediante los breakpoints. Es muy sencillo. Solo hay que seguir los siguientes pasos. No pongo las capturas de pantalla porque creo que ya estoy abusando. ;)
Ponemos los breakpoints en las líneas 32 y 35. Aparte del botón , también se puede hacer con la tecla F9 o, al estilo del Studio 5, con doble clic en las líneas en cuestión. Aparece un punto rojo por cada breakpoint. Simplemente deja que corra el programa (F12). Se detendrá por sí solo en cada breakpoint que encuentre. Bueno cuando se detenga en el breakpoint de la línea 32 reseteas el cronómetro. Ahora vuelve a presionar Run o F12 y el programa correrá hasta detenerse en el siguiente breakpoint, o sea, en la línea 35. Observa el tiempo medido por el cronómetro.
Uso del Osciloscopio Los primeros Osciloscopios de Proteus solo tenían dos canales y presentaban una interface bastante sencilla. Ahora son de cuatro canales, a colores y algo más complejos. Con todo, la compatibilidad con las versiones anteriores se mantiene, y a veces inclusive prefiero las primeras.
Osciloscopio de cuatro canales. Se puede notar una clara distinción de seis recuadros, cuatro de los cuales corresponden a los cuatro canales (Channel) A, B, C y D, con colores distintos.
Los diales Position desplazan las gráficas de cada canal hacia arriba o hacia abajo, en el eje Y. Los diales de tensión fijan la amplitud de voltaje por división. Va desde 2mV/div hasta 20V/div. Con la barra deslizadora de cada canal se marca para que las señales sean tratadas como continuas (DC), solo sus componentes alternas (AC), el nivel de tierra (GND) o para que no aparezca (OFF).
Las opciones del recuadro Trigger se pueden emplear para muestrear las señales de manera que se tengan gráficas más fluidas sin desfases. En realidad, se usa raramente porque este osciloscopio es más bien ideal y poca falta ese tipo de calibraciones. En el recuadro Horizontal el dial inferior establece el tiempo base para los canales, va desde 0.5µs/div hasta 200ms/div. Mientras más pequeño sea más anchas se verán las ondas. Creo que un buen diseño para probar las funciones de este osciloscopio será un sencillo circuito amplificador conformado por un op-amp. Tomaremos las señales de tensión en la entrada y en la salida. El circuito a implementar es el mostrado abajo. Al menos al inicio no te relajes y ármalo tal como está. Al opamp no se le cambia nada, el alternador tiene 1khz de frecuencia y 100mV de amplitud. Donde veo que muchos meten la pata es en las fuentes de alimentación. Si tienes dudas puedes regresar a la sección Edición de las Propiedades de los Objetos.
Circuito de prueba. Al correr la simulación notaremos solo una onda.
Formas de onda en la entrada y en la salida del circuito.
Pero luego de una adecuada calibración de los diales tendremos una imagen así: (Fíjate en la posición de los diales.)
Señales de entrada y la salida reguladas y centradas.
Gráficos de Simulación Dada la potencia del osciloscopio de Proteus, el uso de gráficos para visualizar datos de la simulación podría parecer redundante. Bastará con revisar los ejemplos incluidos en Proteus para darse cuenta de que eso no es del todo cierto. Los gráficos de Proteus son muy parecidos a los obtenidos por programas como PSPICE o Electronics Workbench. De hecho, utilizan los mismos modelos de SPICE y el mecanismo para construirlos suele ser similar, sobre todo en los gráficos analógicos. Sucede que casi todos los modelos digitales de Proteus son nativos. Vamos previendo por tanto que podemos optar por gráficos analógicos o digitales, aunque eso no significa que un gráfico analógico no pueda incluir una señal digital o viceversa. Continuando con el diseño anterior en este ejercicio vamos a capturar las gráficas de las tensiones de entrada y salida del amplificador.
Circuito amplificador simple. Primeramente selecciona la categoría Graph Mode de la barra de herramientas. Del Object Selector escoge ANALOGUE y luego dibuja un rectángulo en el esquemático con el botón izquierdo del mouse. Quedará como el que aparece a la derecha de la siguiente figura.
Pasos para colocar un gráfico de simulación. Ahora vamos a colocar los Probes o sondas de voltaje (también los hay de corriente). Para ello seleccionamos su categoría Voltage Probe y sin darle importancia al Object Selector hacemos clic sobre los puntos del circuito cuyas tensiones tomaremos. Aparecerán esas pequeñas flechitas azules que se conocen como Probes (sondas). Yo solo he puesto dos probes: uno en la entrada y otro en la salida del circuito. Puedes editar las propiedades de los probes como cualquier otro objeto. Como ves, yo les cambié de nombre: Vi para el primero y Vo para el segundo. Es solo para darles algo de identidad.
Colocando sondas en el circuito. Ahora selecciona un probe y con el botón izquierdo arrástralo y suéltalo sobre el rectángulo de la gráfica. Si el probe cayó bien, debió haber aparecido uno similar dentro del rectángulo. Al final verás que el probe original no se ha movido (si lo hizo, es que fallaste ;). Luego haz lo mismo con el otro probe. El rectángulo quedará como éste.
Rectángulo de gráficos con los Probes Vi y Vo. Ahora desplegamos el menú contextual del rectángulo de gráficos y escogemos la opción Maximize (Show Window) para ver las gráficas en una ventana propia. Podemos conseguir el mismo resultado clicando a la franjita verde superior, allí donde dice ANALOGUE ANALYSIS.
Ventana “maximizada” del rectángulo de gráficos. Llegó el momento esperado. Ejecutamos la generación de la gráfica yendo al menú Graph Simulate Graph ya sea en ISIS o en la nueva ventana de gráficos. Momentáneamente la barra de estado de ISIS mostrará el progreso de la simulación y cuando haya finalizado, veremos la gráfica esperada.
Ventana de gráficos con los resultados de la simulación. Bueno, en realidad no tanto. Esperábamos ver ondas sinusoidales, ¿cierto? Debe ser que están tan juntas que no se notan. Si deseas puedes estirar la imagen con los botones de ZOOM disponibles en la parte baja. Una mejor decisión es establecer la simulación de un tramo más corto. Esto se puede configurar en la ventana de propiedades del rectángulo de gráficos en ISIS o aquí mismo yendo al menú Graph Edit Graph… Con esto veremos una ventana como la de abajo. Start time y Stop time determinan el margen temporal de la gráfica presentada. Por defecto valen 0 y 1 con lo que la simulación será en el primer segundo. Es demasiado tiempo. Por eso vimos tantas ondas. Así que vamos a establecer un margen más pequeño, digamos desde Start time=100ms hasta Stop time=1010ms. Con esto tendremos una simulación entre los tiempos 100ms y 110ms.
Configuración de los márgenes de la simulación para gráficos. Volvemos a correr la simulación con el menú Graph rojo corriendo. Y...
Simulate Graph o presionando el botón del hombrecito
Generación de los gráficos en el tramo 100ms - 110ms. Eso sí se ve mucho mejor. Nota que los márgenes de la gráfica dicen 100 ms y 110 ms, tal cual lo indicamos. Los demás detalles de la gráfica, como el uso del cursor, las coordenadas de tiempo y valores de señal en cada punto mostrados en la zona inferior, cambio de colores, etc., los dejo a tu observación. Solo es cuestión de echarle ojo.
Depuración con un Archivo SDI Actualmente incluso los entornos de los ensambladores pueden generar buenos archivos de depuración, aunque no tanto como los generados por los compiladores de alto nivel. Por ejemplo, el MPLAB IDE produce archivos COFF para sus PICs y Studio 5 genera archivos OBJ para sus AVR. Sin embargo, a veces aún podrá resultar útil trabajar con los archivos SDI que Proteus solía emplear antes. Estructuralmente un archivo SDI es muy parecido a un archivo de listado. A diferencia de un COF, que separa los archivos del programa en diferentes campos, un archivo SDI lo presenta todo junto en un solo cuerpo. Esto puede resultar algo incómodo, ya que si trabajamos con varios archivos es precisamente para ordenar las cosas.
En este modo el único archivo a cargar en el microcontrolador es el HEX; el archivo SDI solo tiene que acompañarlo en su carpeta de destino. Para crear un archivo SDI Proteus tiene que ensamblar de nuevo todo el programa por su cuenta. El procedimiento que debemos seguir es: Vamos al menu Source Add/Remove Source Files... Nos aparecerá una ventana como la siguiente. En Target Processor estará seleccionado el nombre del microcontrolador presente en el diseño, es decir, asumimos que a hacer esto partimos con al menos un microcontrolador en el circuito. De lo contrario habrá que seleccionarlo manualmente. Por otro lado, si hay más de un microcontrolador en el circuito, aparecerán todos ellos y nosotros escogemos para cuál será el ensamblado actual (los programas se ensamblan por cada microcontrolador a la vez).
Ventana Add/Remove Source Code Files. Del mismo modo en el campo Code Generator Tool aparecerá el ensamblador a usar. Proteus lo selecciona automáticamente conociendo el microcontrolador del desplegable de al lado. El campo Flags lo dejamos en blanco. Como los ensambladores son pequeños, Proteus tiene copias de unos cuantos, como vinco más o menos. Ahora le damos al botón New y en seguida seleccionamos el archivo de código fuente del programa. En caso de tener un programa comprendido por varios archivos, únicamente debemos seleccionar el principal. El nombre del archivo seleccionado irá al desplegable de arriba. Notaremos que al final del menú Source ahora se ha agregado un ítem con el nombre del archivo ASM. Seleccionando esa opción se abrirá un editor muy parecido al block de notas con el código del programa. Siendo un editor muy pobre, lo puedes cambiar por otro (Notepad++, por ejemplo) seleccionando la opción Setup External Text Editor... Pruébalo cuando tengas tiempo.
Construir el programa main.asm. Para terminar con esto hacemos clic en Build All (Construir todo). Al final quedará una ventana con el mensaje de Source code build completed OK. Solo la cerramos.
Ensamblado del programa. Si nos fijamos en la carpeta del proyecto, veremos que ha aparecido un archivo SDI junto al esperado HEX, entre otros. Solo déjalo allí. Tú sigue cargando el archivo HEX en el microcontrolador. En versiones más
recientes de Proteus esta carga ocurre automáticamente tras el ensamblado. Ya podemos presionar Ctrl + F12.
Depuración con el archivo SDI del programa AVRassembler1. Así mismo, cada vez que se edite el archivo principal del código fuente, Proteus detectará la desactualización del archivo SDI y ensamblará todo el programa de nuevo.
Qué es el Studio 5 El Studio 5 es el Entorno de Desarrollo Integrado de Atmel para el desarrollo de proyectos con varios de sus productos, relacionados con sus microcontroladores. Entre las herramientas que incluye nos deben interesar las siguientes:
Un editor de códigos, para editar los programas. Como todo gran editor permite mostrar los códigos fuente a colores, con números de línea, etc. Un administrador de proyectos, que además de trabajar con programas en ensamblador, le da un completo soporte a los compiladores GCC AVR32 y GCC AVR (WinAVR). A diferencia de versiones anteriores ahora es más difícil la integración con compiladores comerciales como CodeVision AVR o ImageCraft AVR. El ensamblador AVRASM, para trabajar con programas en ensamblador. Los compiladores de software libre GCC AVR y GCC AVR32 en su versión para Windows (WinAVR), para desarrollar programas en C para los AVR de 8 y 32 bits, como los ATtiny, ATmega, ATxMega y AVR32. En versiones pasadas del Studio 5, este compilador se debía instalar por separado. El simulador AVR Simulator, para simular los programas de los AVR tanto si están escritos en lenguaje C o ensamblador. El paquete AVR Software Framework o ASF, que es un conjunto de más de 400 proyectos de ejemplo en lenguaje C para los AVR de 8 y de 32 bits, desde el uso de puertos hasta el control del puerto USB. Un completo sistema de ayuda integrado.
El Studio 5 también incluye las siguientes herramientas, las cuales son de uso opcional porque requieren del hardware correspondiente. De todos modos, si tú puedes conseguirlos o deseas saber más de ellos, puedes visitar la web de Atmel.
Los softwares de programación como AVR Dragon, AVRISP MkII o AVR ONE!. Éstos son programas que trabajan con dispositivos programadores comerciales del mismo nombre.
Los programadores AVRISP MkII (izquierda) y AVR ONE! (derecha).
Un potente depurador llamado JTAGICE mkII. ICE significa In Circuit Emulator, y hace referencia a un sistema de depuración en el mismo circuito y con el mismo microcontrolador. Obviamente debe trabajar con su propio adapatador hardware, que se conecta a la PC vía la interface JTAG, conformada por los pines TMS, TCK, TDI y TDO del AVR. Esta interface también permite la programación serial del AVR, es decir, el hardware es un depurador y programador. No todos los AVR tienen soporte JTAG ICE (ejemplo los ATtiny).
El depurador/programador JTAGICE mkII.
El software JTAGICE mkII puede dirigir el programa del AVR en la misma aplicación paso a paso, como en cámara lenta o en tiempo real, e ir visualizando los resultados de las operaciones a medida que se van ejecutando. De ese modo se puede monitorizar y analizar “en vivo y en directo” cómo van cambiando los recursos hardware del AVR, como el contenido de sus memorias y de sus registros internos. Para quienes conozcan algo, es parecido a la simulación de Proteus o del mismo Studio 5, pero esto será real. Los elementos hardware de control y programación del AVR también suelen estar disponibles en las tarjetas de desarrollo que provee ATmel, como las tarjetas STK500 o STK600. Las utilidades software correspondientes están incluidas en el Studio 5.
La tarjeta de desarrollo STK600.
Descarga del Studio 5 y WinAVR
Ahora que ya tienes una idea bien formada de lo que es y lo que puede hacer el Studio 5, puedes descargarlo libremente desde www.atmel.com. Puede pesar hasta más de 600 MB, así que tendrás que esperar un poquito. Si ya tienes este programa, pero en su versión 4.x, será mejor que te actualices. Hay sustanciales diferencias entre la versión 5.x y las anteriores, en las cuales no perderé tiempo citándolas. Solo espero que luego no te quejes si encuentras cosas que no te salen igual ;). En la página de descarga encontrarás los siguientes paquetes.
AVR Studio 5 Installer (includes VSS and .NET 4.0). Como allí se indica, esto incluye los paquetes Microsoft Windows Framework .NET 4.0 y Visual Studio Shell (Isolated Mode) 2010. Ambos son prerrequisitos para la instalación del Studio 5 y probablemente ya los tengas instalados en tu PC. Si no estás seguro de ello o si prefieres perder un poquito más de tiempo en la descarga antes que averiguarlo, puedes descargar este paquete completo. El instalador trae incluidos los compiladores GCC AVR y GCC AVR32 (WinAVR). AVR Studio 5 Installer. Contiene únicamente el instalador del Studio 5 (incluyendo GCC AVR32 y GCC AVR [WinAVR]). Descarga esta opción si ya tienes Microsoft VSS y .NET 4.0. AVR Studio 5 – AVR Software Framework Update. Este paquete contiene la actualización de los más de los más de 400 proyectos, ejemplos, librerías, etc., que conforman lo que se llama el AVR Software Framework o simplemente ASF. El instalador del Studio 5 ya lo trae incluido solo que quizá no en su versión más actual. Por ejemplo, cuando descargué mi Studio 5.0 incluía el ASF 2.5.1 mientras que el actual era 2.7.0. La verdad es que la diferencia en el contenido no se percibe. Descarga este archivo exe solo si quieres estar súper actualizado e instálalo luego del Studio 5.
AVR Studio 5.0 - Part Support Pack for AVR XMEGA with USB. Es una extensión para que el Studio 5 también soporte los nuevos AVR ATxMega con USB, en este caso los ATxmega16A4U, ATxmega32A4U, ATxmega64A3U, ATxmega128A3U, ATxmega192A3U, ATxmega256A3BU y ATxmega256A3U. La lista debe cambiar con el tiempo. Descarga el instalador si trabajas con estos AVR.
Instalación del Studio 5 y WinAVR Requisitos de sistema
Windows XP (x86) con Service Pack 3 en todas las ediciones excepto Starter Edition. Windows Vista (x86) con Service Pack 1 en todas las ediciones except Starter Edition. Windows XP (x64) con Service Pack 2. Windows Vista (x64) con Service Pack 1. Windows 7 (x86 y x64). Windows Server 2003 R2 (x86 y x64). Ordenador con procesador de 1.6GHz o superior. 1 GB de RAM para x86. 2 GB de RAM para x64. 512 MB de RAM adicionales si se trabaja en una PC virtual. 3GB de espacio disponible en el disco duro. Disco duro de 5400 RPM o superior. Tarjeta de vídeo con soporte DirectX 9 y resolución de 1024 x 768 o superiores.
Primero debo hacer una salvedad y es que antes de instalar el Studio 5 versión Release debes desinstalar la versión Beta, si es que lo tenías. Y si tienes instalado AVR Studio 4 o AVR32 Studio, los puedes conservar porque el Studio 5 puede trabajar en paralelo con ellos. El Studio 5 ya no soporta los siguientes dispositivos, que aún están disponibles en el Studio 4, ATtiny11, ATtiny12, ATtiny15, ATtiny22, AT90S1200, AT90S2313, AT90S2323, AT90S2343, AT90S4433, AT90S8515, AT90S8535, ATmega323, ATmega161, ATmega163, ATmega103, ATmega165, ATmega169, ATmega406, ATmega16HVA, ATmega16HVA2, ATmega64HVE, ATmega32U6, AT90PWM2, AT90PWM3, AT90SCR100, AT86RF401. Bueno ahora sí empezamos la instalación como cualquier otro programa de Windows, siguiendo las indicaciones presentadas. Aquí, algunos screenshots:
Lista de los requisitos software. Antes de instalar el Studio 5 propiamente se instalarán los paquetes listados. Para quienes ya tenían instalados Microsoft .NET Framework 4.0 (se supone que Windows 7 Ultimate ya lo incluye) o Microsoft Visual Studio 2010 Shell, dichas opciones ya no estarán disponibles y por tanto pasarás directamente a la instalación de AVR Jungo USB Driver.
Instalación de Microsoft Visual Studio 2010 Shell Luego vendrá la instalación de Microsoft Visual Studio 2010 Shell. Si tienes Internet y deseas enviar a Microsoft información sobre tu experiencia con la instalación de Visual Studio, puedes activar la casilla indicada.
Terminada esta parte nos dirán que Microsoft Visual Studio 2010 Shell se instaló con éxito, a pesar de que pueden aparecer esas X en rojo indicando que aún no se ha instalado la documentación respectiva (no es necesario para el desempeño del Studio 5 pero si deseas la puedes descargar desde la web de Microsoft). También me recomienda actualizar mi PC con los parches de seguridad más recientes de Windows.
Instalación de AVR Jungo USB Y ahora viene la instalación del driver USB de Jungo para los AVR. Studio 5 utiliza estos driver para manejar sus tarjetas hardware con interface USB. Si ya los tenías instalados, digamos porque trabajaste con versiones anteriores del AVR Studio, entonces este procedimiento actualizará los drivers con la versión presente. Los drivers USB de Jungo son compatibles con sus antecesores.
Por supuesto que los drivers de Jungo son confiables, son de los mejores.
Instalación del Studio 5 Y pensar que recién vamos a instalar el Studio 5 propiamente (creo mi Windows se ha instalado más rápido ;).
Entorno del Studio 5
Entorno de desarrollo de Studio 5. Ésa es la distribución por defecto de las ventanas del Studio 5. En primer plano tenemos a Start Page o Página de Inicio, a la derecha está la ventana Solution Explorer y abajo, la ventana Output. Hay más ventanas que iremos conociendo en el camino pero mientras te familiarizas con el entorno puedes reacomodarlas según tu preferencia. Para ello puedes arrastrarlas tomándolas por su barra de títulos y colocarlas donde indica el guía o puedes dejarlas flotando. Esto último no suele ser muy útil, aunque a mí me servirá para mostrar las capturas de pantalla más adelante
Reubicación de las ventanas del entorno del Studio 5. Hacer estas reubicaciones es divertido sobre todo si tenemos en cuenta que podemos regresar a la distribución inicial yendo al menú Window Reset Window Layout. Y si queremos volver a tener la página Start Page para ver a la mariquita de nuevo vamos al menú View Start Page. Nota que esta opción también tiene un icono en la barra de herramientas. Es el menú View donde también puedes encontrar las otras ventanas y no en el menú Window. Antes que mostrar una típica descripción de cada uno de los elementos del entorno del Studio 5 los iré presentando a medida que hagan su aparición y sean requeridos.
Trabajando con Proyectos y Soluciones en C A diferencia de los compiladores Basic, donde basta con crear un archivo BAS suelto y luego compilarlo, los programas en C siempre forman parte de un proyecto. Actualmente desarrollar proyectos en C con el Studio 5 es más sencillo que en versiones anteriores debido en gran parte a que está adaptado para trabajar especialmente con los compiladores libres GCC AVR32 y GCC AVR (WinAVR). Una Solución (Solution) es un conjunto formado por uno o varios proyectos, así que todo proyecto debe pertenecer a alguna Solución. El Studio 5 puede trabajar con uno solo o con todos los proyectos de la Solución al mismo tiempo, pero solo puede administrar una Solución a la vez. Hay más consideraciones respecto a los Proyectos y las Soluciones, pero creo que será mejor describirlas mientras creamos el Proyecto.
Creación de un Proyecto en C Solo hay una forma de crear un proyecto, esto para simplificar las cosas. Abierto el Studio 5 vamos al menú File
New Project o hacemos clic en New Project de Start Page.
Creando un proyecto desde Start Page. De cualquier modo llegaremos al mismo asistente, donde empezamos por seguir lo que indica la siguiente figura. El nombre y la ubicación del proyecto pueden ser los que desees.
Elección del nombre y ubicación del proyecto. Ten en cuenta que el Studio 5 creará una nueva carpeta (con el nombre del proyecto) dentro de la ubicación indicada. Observa que el nombre de la Solución es el mismo que el del proyecto. Todo proyecto debe pertenecer a alguna Solución, y como estamos creando el primer proyecto, el Studio 5 le asignará una Solución automáticamente. Cuando creemos los siguientes proyectos tendremos la opción de elegir si pertenecerán a ésta o a una Solución nueva. Si activas la casilla Create directory for Solution, la Solución (que es un archivo a fin de cuentas) se alojará en la carpeta que debía ser para el proyecto, y el proyecto junto con sus archivos generados irán a parar a una sub carpeta con el mismo nombre. Esto puede ser conveniente cuando se tiene una Solución con varios proyectos. Yo sé que parece enredado pero con un par de prácticas lo entiendes mejor y te acostumbras. Normalmente prefiero tener un proyecto por cada Solución, de modo que no tengo que marcar la casilla indicada y puedo tener todos los archivos del proyecto y de la Solución dentro de la misma carpeta sin que se confundan. Es así como están conformadas todas las prácticas de cursomicros.com. Le damos clic a OK y tendremos una ventana mucho menos traumática. Solo seleccionamos el AVR con el que trabajaremos. En el futuro podremos cambiar este microcontrolador por otro. También puedes aprovechar
esta ventana para reconocer las memorias de cada dispositivo y para ver las herramientas que tienen disponibles desde el Studio 5.
Elección del microcontrolador del proyecto. En seguida tendremos nuestro proyecto con el Editor de Código mostrándonos el archivo de código fuente principal listo para que lo editemos. Observa que el Explorador de la Solución o Solution Explorer muestra los Proyectos de la Solución así como los archivos de cada Proyecto. Debajo está el marco Properties que informa las propiedades de cada archivo seleccionado arriba, como su ubicación. De todas las ventanas aquí mostradas o las que puedan surgir en adelante la que no deberías perder de vista es el Explorador de la Solución. Desde allí accedes a todos los archivos del proyecto (los abres con doble clic) y también puedes construir el proyecto así como establecer su configuración. Si se te llega a perder esta ventana la puedes visualizar yendo al menú View Solution Explorer.
Esquema del proyecto creado.
Edición del Código Fuente El editor de códigos se llama simplemente Code y es accesible desde el menú View. También se muestra automáticamente cada vez que abrimos un archivo. Como lo dijimos al inicio, la podemos cerrar, reubicar o dejar flotando como se ve en la siguiente figura. El código mostrado corresponde al programa del LED parpadeante ledflasher3, que es con el que vamos a trabajar. Todavía no lo podemos compilar porque faltan agregar al proyecto los archivos mencionados en las directivas include. Hablamos de los archivos avr_compiler.h, usart.h y usart.c. Pero eso lo veremos en la siguiente sección.
Edición del programa Ledflasher3.
Adición de Archivos o Librerías al Proyecto Los proyectos en AVR GCC o IAR C raras veces constan de un solo archivo. Casi siempre hay archivos de configuración o librerías que añadir. De hecho, en cursomicros.com todas las prácticas incluyen al menos el archivo avr_compiler.h, el cual permite que los códigos de programa se puedan construir indistintamente para los compiladores AVR GCC o IAR C. Este archivo forma parte del paquete ASF y lo puedes hallar en el directorio de instalación del Studio 5. También puedes encontrar una copia suya (con ligeras modificaciones) en todos los proyectos de cursomicros.com.
Las librerías en el lenguaje C se suelen dividir en dos archivos: uno (con extensión .c) que suele contener los códigos ejecutables de las funciones y otro (con extensión .h) donde se escriben las definiciones y los prototipos de las funciones, básicamente. Así por ejemplo, en cursomicros.com se usan las librerías para displays LCD (con archivos lcd.h y lcd.c), para el puerto serie (con archivos usart.h y usart.c) o para el bus I2C (con archivos i2c.h e i2c.c). En los códigos fuente los archivos se invocan mediante la directiva include. Adicionalmente en los proyectos de WinAVR o IAR C deben incluirse desde sus entornos de desarrollo. En esta ocasión veremos como hacer esto con los archivos avr_compiler.h, usart.h y usart.c y el procediemiento descrito es el mismo que debes seguir cuando añadas otros archivos. Una vez ubicados los archivos avr_compiler.h, usart.h y usart.c, colócalos en la carpeta de tu proyecto. Esto no es necesario porque que se le podría incluir dondequiera que esté desde el Explorador de la Solución. Sin embargo habrá proyectos en los que se quiera editar el archivo incluido y para que dichos cambios no afecten a los demás proyectos lo más recomendable será tener una copia suya en la carpeta de cada proyecto, como se ve abajo.
Los archivos avr_compiler.h, usart.h y usart.c deberían estar en la carpeta del proyecto. Ahora debemos indexar el archivo al proyecto. Para ello seleccionamos el proyecto en el Explorador de la Solución y vamos al menú Project Add Existing Item… También puedes emplear mi camino preferido, que se ilustra a continuación.
Indexando los archivos avr_compiler.h, usart.h y usart.c al proyecto. Observa que ahora el archivo añadido también se visualiza en el Explorador de la Solución.
Archivo avr_compiler.h indexado.
Construcción del Proyecto Antes de entrar en el proceso de la construcción debemos saber que existen dos modos básicos de configuración en que se generarán los resultados, que son Debug y Release. El modo por defecto es Debug. Uno de los efectos de cambiar del modo Debug al modo Release es que la optimización del código se adaptará para obtener el archivo HEX de menor tamaño. Esta optimización es necesaria para que las funciones de delay del programa queden mejor afinadas.
Elección entre los modos Debug y Release. También debemos considerar que cada modo generará los archivos de salida en sus correspondientes carpetas. Hasta ahora solo hemos visto la carpeta Debug pero al compilar el proyecto en modo release se creará también una carpeta Release. Una opción para evitar marearnos entre estos modos es ajustar la optimización del código directamente. Aprendemos eso en una próxima sección. De momento cambiemos a release y sigamos. Bueno, para construir un proyecto podemos tomar varios caminos, dos de los cuales se muestran abajo. Un tercer camino es seleccionar el proyecto en el Explorador de la Solución, hacer clic derecho y en el menú emergente aparecerán las mismas opciones del menú Build.
Build Solution (construir la Solución). Una Solución puede estar compuesta por varios proyectos. De ser el caso, con esta opción se construirán todos los proyectos de la Solución. Rebuild Solución (reconstruir la Solución). Reconstruye todos los proyectos de la Solución haciendo previamente una limpieza de los archivos de salida generados antes. Clean Solution (Limpiar Solución). Para todos los proyectos de la Solución actual, limpia o borra todos los archivos de salida, como HEX, LSS, MAP, etc., que se crearon durante una construcción previa.
Build ledflasher (construir ledflasher). En el entorno del lenguaje C construir en realidad involucra varios procesos, como compilar, enlazar (linkar) y ensamblar. Normalmente nos referimos a esta acción simplemente como compilar. Rebuild ledflasher (reconstruir ledflasher). Vuelve a construir el proyecto indicado pero haciendo una limpieza previa de sus archivos de salida. Clean ledflasher (limpiar ledflasher). Borra todos los archivos de salida del proyecto indicado. Compile (compilar). La opción compilar aparece en el menú Build cuando se selecciona un archivo C. Al compilar un archivo no obtendremos los archivos HEX o ELF, sino solo un archivo objeto, que es previo al HEX o ELF.
Puesto que nuestra Solución de ejemplo solo tiene un proyecto, dará lo mismo si elegimos construir el proyecto o construir la Solución. Tras construir el proyecto del LED parpadeante nos aparecerá la ventana de salida Output con los resultados de éxito, como se aprecia abajo. Probablemente tu ventana Output aparezca en otra disposición pero como aprendimos antes, eso es solo cuestión de reubicación.
Resultados de la construcción. Entre los archivos de salida puedes ver que aparecieron los esperados HEX (para grabar el microcontrolador) y ELF (para la depuración o simulación en programas como Proteus). Todos estos archivos se depositan en la carpeta Release del proyecto porque lo compilamos en ese modo.
Lo archivos de salida van a las carpetas Release o Debug.
Renombrar los Archivos del Proyecto Por defecto los nombres de la Solución, del proyecto y del archivo de código fuente principal son el mismo. Esto es opcional, pero yo prefiero que mi archivo de código principal se llame siempre main.c para hacer una mejor distinción de los otros archivos de código como las librerías. Renombrar un archivo es sencillo. Solo hay que seleccionarlo en el Explorador de la Solución, abrir su menú contextual (clic derecho) y escoger la opción Rename. Del mismo modo también es posible cambiar de nombre el proyecto y la Solución.
Renombrando los archivos del proyecto y la Solución.
Cambiar la Frecuencia del Procesador Para WinAVR normalmente las únicas rutinas que requieren la definición de la frecuencia del procesador son las funciones de delay, que se encuentran en los archivos del mismo nombre disponibles en la carpeta utils del WinAVR. Quienes utilizan dichos delays suelen editar la constante F_CPU definida en esos archivos. Pero el archivo avr_compiler.h también define la constante F_CPU antes de utilizar las funciones de delay de WinAVR. De modo que si vamos a trabajar con este archivo, es aquí donde debemos editar la constante F_CPU, que casi siempre coincidirá con la frecuencia del XTAL usado. Gracias a las macros de avr_compiler.h F_CPU también tendrá efecto en las funciones de delay del compilador IAR AVR C. Además, las librerías de www.cursomicros.com para los módulos síncronos del AVR como el USART o TWI (I2C) también se basan en F_CPU. Queda claro entonces que no solo la utilizo para calibrar los delays. En el archivo avr_compiler.h del paquete ASF la constante F_CPU se define a 11059200Hz. Pero en casi todas las copias de avr_compiler.h disponibles en www.cursomicros.com F_CPU está redefinida a 8 MHz
(como se aprecia en el siguiente extracto de dicho archivo) porque en la gran mayoría de mis diseños trabajo con esa frecuencia. //////////////////////////////////////////////////////////////// #ifndef F_CPU /* Define la frecuencia de CPU (en Hertz) por defecto, si no ha * sido definida. */ #define F_CPU 8000000UL // XTAL de 8 MHz #endif
Reconfiguración del Proyecto WinAVR El compilador AVR GCC nació inicialmente para Linux y con el tiempo se desarrolló la versión para Windows llamada WinAVR. Hasta ahora WinAVR no tiene un entorno propio y de hecho ya no hace falta gracias al Studio 5. Pero anteriormente para usar WinAVR había que invocarlo desde otros entornos como CodeBlocks, Eclipse o el mismo Studio 4. Esos entornos no eran del todo compatibles con WinAVR y más temprano que tarde uno tenía que configurar las opciones del proyecto en el mismo archivo Makefile. Makefile es un archivo de texto lleno opciones para la construcción del proyecto. WinAVR sigue trabajando en base a él, solo que para nosotros el Studio 5 lo crea y edita automáticamente. Cada cambio realizado en la ventana de propiedades del proyecto es reflejado en el archivo Makefile. Si te interesa echarle un vistazo, puedes encontrar Makefile entre los archivos de salida en las carpetas Debug y/o Release. Para acceder a la ventana de propiedades del proyecto, lo seleccionamos y vamos al menú Project Properties. Yo, como siempre, prefiero ir por el Explorador de la Solución, como se ve abajo.
Hay varias categorías desde Build hasta Advance y en muchas de ellas podremos elegir si la configuración se aplica al modo Debug o Release. Recuerda que cada modo hace que los archivos de salida (incluyendo el Makefile) vayan a sus respectivas carpetas.
Ventana de propiedades del proyecto.
Optimización del Código Comúnmente los compiladores C ofrecen varios niveles de optimización del código. Con WinAVR tenemos los siguientes niveles:
No optimizar (nivel O0). El código no se optimiza nada. Por ejemplo, el programa del LED parpadeante compilado con este nivel resulta en un código de 3714 bytes y compilado en nivel Os resulta en 118 bytes. Sí que hay diferencia, ¿verdad? Sin embargo, muchas veces se usa este modo con fines de depuración porque genera el mejor archivo ELF. No por nada es el nivel por defecto del modo Debug. Además, esa diferencia se desvanece cuando se trabaja con programas grandes. Por ejemplo un programa que utiliza funciones matemáticas con números de punto flotante y la función Printf en su máxima performance a mí me resultó en 3366 bytes sin optimización y 3038 con optimización Os. Optimizar (nivel O1). Optimiza regularmente; bastante, comparado con el nivel O0. Con este nivel mi programa de prueba resultó en 3066 bytes. Optimizar más (nivel O2). Con este nivel mi programa de prueba resultó en 3060 bytes. Optimizar al máximo (nivel O3). Este nivel es particular porque no optimiza para conseguir el menor código, sino el más veloz, es decir, el código que se ejecuta en el menor tiempo posible. Para esto el compilador intentará incrustar en línea todas las funciones que pueda, como las que se llaman solo una vez. Siguiendo con el ejemplo, mi programa resultó en 3102 bytes.
Optimizar para tamaño (nivel Os). Optimiza para que la construcción genere el menor código máquina posible. En general el tamaño del código maquina se contrapone a su velocidad de ejecución. No se puede conseguir un código mínimo y que al mismo tiempo sea el más veloz. El manual de WinAVR recomienda utilizar este nivel combinado con la directiva -mcall-prologues, excepto cuando se quiera usar el nivel O3 para ganar algo de velocidad. El nivel Os es la opción por defecto del modo Release.
Ahora abramos la ventana de propiedades del proyecto y ubiquémonos en Toolchain Compiler Optimization.
AVR/GNU C
Recuerda que puedes elegir si la configuración se aplica al modo Debug o Release y que cada modo hace que los archivos de salida vayan a sus respectivas carpetas.
Cambio de la optimización del código. Había señalado anteriormente que para mejorar la optimización de código en tamaño la ayuda de WinAVR recomendaba el uso de la opción -mcall-prologues. Dice que con esto se usarán subrutinas especiales para guardar y restaurar los registros en las entradas y salidas de las funciones que usen muchos registros, a costa de un pequeño incremento en el tiempo de ejecución. (Es de suponer que se trata de bucles, ¿cierto?)
En fin, sea como fuere, para habilitar la opción -mcall-prologues en el Studio 5 ni siquiera tenemos que escribirla, simplemente activamos la casilla Use subroutines for function prologues and epilogues, como de ilustra en la siguiente figura.
Cuando un programa se construye exitosamente no siempre significa que lo hizo de la mejor forma. A veces puede haber algunos mensajes o advertencias que no se muestran en la ventana de salida Output. Para ver en detalle estos errores, advertencias y mensajes debemos inspeccionar la ventana Error List, que está disponible desde el menú View Error List.
Lista de errores, advertencias y mensajes de la construcción. Aquí muestro la ventana flotante pero recuerda que la puedes anclar donde desees. El caso es que la advertencia mostrada es muy frecuente. Señala que las optimizaciones del compilador están inhabilitadas y que las funciones del archivo delay.h no funcionarán adecuadamente. Imagino que con todo lo visto aquí a ti
nunca se te debería presentar. Nosotros no invocamos al archivo delay.h directamente, sino a través de avr_compiler.h.
Cambiar de Microcontrolador AVR Observa que ni en WinAVR ni en otros compiladores como IAR AVR C o ImageCraft AVR se debería seleccionar el AVR del proyecto desde el código fuente, como se suele hacer en Basic. Si queremos conocer el microcontrolador AVR para el que está hecho un proyecto o si queremos cambiarlo, debemos ir a la categoría Device.
Reelección del microcontrolador del proyecto. Nos aparecerá una ventana muy parecida a la que teníamos cuando creamos el proyecto. La diferencia es que ya no están los AVR de 32 bits, así que el cambio solo es factible por otro AVR de 8 bits. Lo mismo ocurre cuando administramos un proyecto con un AVR de 32 bits. Entendemos que para cambiar de microcontrolador primero tendríamos que cambiar de compilador, de AVR GCC a AVR32 GCC o viceversa, pero eso tampoco es posible desde el Studio 5. La única forma sería creando un nuevo proyecto.
Cambio del microcontrolador del proyecto.
Uso de un Archivo Makefile Externo Explicamos anteriormente lo que significa el archivo Makefile para el compilador WinAVR y que en estos días sería muy raro editarlo manualmente. Sin embargo, dada la gran flexibilidad de WinAVR, a veces incluso administrar la ventana de propiedades del proyecto en el Studio 5 puede resultar confuso. Has de saber que a diferencia de otros compiladores C, la configuración de compilación de un proyecto en WinAVR reside íntegramente en su archivo Makefile. Los archivos de Proyecto y de la Solución que crea el Studio 5 se reducen a simples cascarones en comparación con Makefile. Si alguna vez tienes el código fuente de un gran proyecto ajeno pero sin su Makefile, probablemente no logres compilarlo como el autor lo diseñó, si es que no utilizas la misma configuración. Tendrías que acertar con la configuración exacta o conseguir el archivo Makefile original. Bueno, ya sea que quieras trabar con un archivo Makefile o que simplemente seas uno de los antiguos nostálgicos que extrañan editarlo a mano (como yo ;), la forma de usar un archivo Makefile diferente del creado por el Studio 5 es:
Uso de Makefile externo en Studio 5.
Configurar el Uso de Printf ¿Quieres visualizar números de punto flotante y solo obtienes el signo ? ? Imagino que esto debe ser frustrante para los usuarios de los compiladores “fáciles” como CCS C. Espera un momento. Ni el lenguaje C ni el compilador GCC fueron diseñados para los microcontroladores. Son muy útiles las familias de funciones printf y scanf pero al mismo tiempo bastante pesadas para la arquitectura de los microcontroladores, sobre todo si son de 8 bits y no tienen chip matemático. Por las limitaciones conocidas hasta los mejores compiladores como IAR C o WinAVR tienen que subdividir a printf y scanf y sus derivados en tres versiones, para que el usuario escoja la que más se ajuste a su demanda y a las restricciones de su microcontrolador.
La versión minimizada de printf restringe los especificadores de conversión al uso de enteros de 16 bits (entre octales, hexadecimales y decimales positivos o negativos), de caracteres y de cadenas de caracteres. No están disponibles los números de punto flotante ni los números enteros de 32 bits. No se permite especificar el ancho de los números y por ende tampoco se disponen de las opciones de justificación ni de relleno con blancos o ceros.
Según el manual de avr-libc, la versión minimizada de printf se consigue estableciendo las siguientes opciones de linkador:
-Wl,-u,vfprintf -lprintf_min
La versión predeterminada de printf ofrece todas las características de la versión completa excepto las relacionadas con los números de punto flotante. En los megaAVR demanda cerca de 350 bytes más que la versión minimizada. Es redundante decir que ésta es la versión por defecto y no requiere de ninguna instrucción al linkador. Pero si previamente se hubo establecido alguna de las otras versiones, entonces bastaría con quitar las siguientes opciones para regresar a la versión por defecto de printf.
-Wl,-u,vfprintf La versión completa de printf brinda todo su potencial, incluyendo las conversiones de números de punto flotante. En los megaAVR requiere cerca de 380 bytes de memoria FLASH más que la versión predeterminada. La versión completa de printf viene con las siguientes opciones de linkador:
-Wl,-u,vfprintf -lprintf_flt -lm
Las preguntas son: ¿y dónde se escriben esas “cosas”?, o ¿hay que escribirlas? ¿Por qué no se arregla simplemente con un clic?, o ¿por qué no es como en CCS C, o aunque sea como en CodeVisionAVR? Y las respuestas son: porque WinAVR sigue siendo un huésped del Studio 5 y todavía no tienen una mejor interacción. Tal vez en el futuro. Por otro lado, de hecho CCS C también utiliza varias plantillas de su función printf con diferente alcance cada una, solo que antes de compilar el programa detecta las características mínimas necesarias según el código fuente y selecciona automáticamente la plantilla más adecuada. Genial idea, ¿verdad? Me quito el sombrero. Sospecho que CodeVisionAVR está cerca de conseguir esa funcionalidad. Por su parte, las recientes versiones de IAR C ya cuentan con una opción Auto para este propósito.. Retomando el tema, lo que hacen esas “cosas” son dos cosas (como en El gato ;): primero, le dicen al linkador que no utilice la versión de printf por defecto, así: -Wl,-u,vfprintf -Wl indica que la directiva va para el linkador y –u,vfprintf le dice que desconozca el símbolo vfprintf (que representa la función printf y similares) que se encuentra entre las librerías que usa por defecto y que más bien lo busque en otras librerías. ¿Dónde? Para la versión minimizada, en la librería indicada por: -lprintf_min Y para la versión completa, en las librerías indicadas por: -lprintf_flt -lm Según el manual de GNU GCC (que también se instaló con tu WinAVR), la opción genérica –llibrary se refiere a la librería liblibrary.a, por lo que en realidad las librerías mencionadas son libprintf_min.a , libprintf_flt.a y libm.a.
Si todavía sigues ahí, selecciona tu proyecto en el Studio 5, muestra su ventana de propiedades, ve a la categoría Toolchain AVR/GNU C Compiler Miscellaneous y al lado de Other Linker Flags escribe -Wl,u,vfprintf, tal como aparece abajo.
Indicar a WinAVR que no use el printf por defecto. Ahora nos ubicamos en Toolchain libprintf_min.a, así:
AVR/GNU C Compiler
Libraries y añadimos la librería
Añadiendo librerías para el linkador en el Studio 5. Del mismo modo, agrega las librerías libprintf_flt.a y libm.a. Te debería quedar así:
Indicar a WinAVR que primero busque [printf] en libprintf_flt.a (versión completa). Ahora ya podemos usar printf en sus versiones minimizada o completa. Fíjate bien en el orden de las librerías añadidas. Si en la lista aparece libprintf_flt.a antes que libprintf_min.a, entonces se utilizará la versión completa de printf y similares, como se ve arriba. Pero si la librería libprintf_min.a tiene mejor nivel que libprintf_flt.a, entonces se usará la versión minimizada de printf. Esto se ve abajo. Para no confundirte en cualquiera de los casos puedes quitar la librería que no utilices. Puse este esquema solo para no estar añadiendo librerías a cada rato y para mostrar sus prioridades de llamada. La posición relativa de libm.a no importa, pues solo contiene rutinas matemáticas para printf en su versión completa.
Indicar a WinAVR que primero busque [printf] en libprintf_min.a (versión minimizada).
Configurar el Uso de Scanf La familia de funciones scanf es mucho menos recurrente que printf. A veces su empleo es inclusive contraindicado debido a que no ofrece buenos filtros. Scanf también viene en tres versiones:
La versión minimizada admite el ingreso de caracteres, cadenas de hasta 256 caracteres y números enteros de 16 bits positivos o negativos. No maneja números de punto flotante, no admite el especificador [, usado para filtrar el ingreso según un conjunto de caracteres indicado por el usuario. Tampoco permite usar el flag de conversión %. Para su uso se requieren las siguientes opciones de linkador. Allí se indican inhabilitar la versión predeterminada e incluir las librerías libscanf_min.a y libm.a. -Wl,-u,vfscanf -lscanf_min -lm La versión predeterminada provee casi todas características de scanf en versión completa. No ofrece conversiones de números de punto flotante y el ingreso de caracteres se limita a 256. No hacen falta opciones de linkador. Si se estableció antes otra versión, habría que remover al menos las opciones: -Wl,-u,vfscanf La versión completa sí permite las conversiones de números de punto flotante.
Para su uso debemos usar las siguientes opciones de linkador. Allí se indican inhabilitar la versión predeterminada e incluir las librerías libscanf_flt.a y libm.a. -Wl,-u,vfscanf -lscanf_flt -lm
Ya vimos que agregar librerías y poner las opciones –Wl, u, vfscanf es muy sencillo. El punto será cómo combinar las opciones de linkador si se usan al mismo tiempo ambas familias de printf y scanf en versiones no predeterminadas. Cualquiera de las siguientes dos formas vale (no importa el orden entre vfscanf y vfprintf): -Wl,-u,vfscanf -Wl,-u,vfprintf -Wl,-u,vfscanf -u,vfprintf
Indicar a WinAVR que no use el scanf por defecto.
Indicar a WinAVR que no use ni el printf ni el scanf por defecto. No es necesario que estén presentes las dos librerías libscanf_flt.a y libscanf_min.a al mismo tiempo, pero al estar presentes las dos, se deberá tener en cuenta el orden en que aparecen, igual que para printf. Por ejemplo, de acuerdo con la siguiente figura, scanf funcionará en su versión completa porque su librería está primero, y printf también pero porque no está la librería de la versión minimizada.
Indicar a WinAVR que primero busque [scanf] en libscanf_flt.a (versión completa).
Simulación del Programa El simulador AVR Simulator permite monitorizar el curso y evolución del programa paso a paso, es decir, ejecutar el programa instrucción por instrucción si fuera necesario y visualizar el contenido de los puertos de E/S, las variables de programa, el valor de las locaciones internas de la RAM incluyendo los registros de E/S, los Registros de Trabajo, el estado de la Pila y valor actual del Contador de Programa. También se puede medir el tiempo transcurrido entre distintos puntos del programa o simular la respuesta del programa ante ciertos eventos detectados en los puertos de E/S del AVR. Se ve genial, ¿verdad? Sin embargo, no es mucho comparado con un simulador del calibre de Proteus VSM. Proteus puede hacer todo lo mencionado arriba y que veremos en seguida, pero mucho mejor. Proteus no solo simula el programa del AVR, sino que nos permite examinar con mayor acercamiento su interacción con el circuito de aplicación, y muchas veces en tiempo real. Lo cierto es que Proteus VSM no es un programa gratuito, aunque debemos reconocer que bien vale su precio, siendo, de lejos, el mejor simulador de circuitos con microcontroladores que existe ―al menos que yo conozca―. Así que si estás entre los afortunados que pueden conseguir Proteus, puedes leer el capítulo que se le dedica en vez de las siguientes páginas. A propósito, la versión demo disponible en su web www.labcenter.co.uk no permite simular más que los ejemplos que trae incluidos y algunos circuitos muy sencillos creados por el usuario; de poco nos serviría.
Para esta sección practicaremos con el pequeño secuenciador de LEDs ledflasher3. Todas las prácticas de cursomicros.com son proyectos con Solución separada, así que da lo mismo si abres el archivo del proyecto o de la solución. Puedes abrir el proyecto o solución desde el Studio 5 o con doble clic en cualquiera de sus archivos, *.avrgccproj o *.avrsln, respectivamente.
Entorno del Studio 5 con el programa de ledflasher3. Para iniciar el AVR Simulator vamos al menú Debug Start Debugging and Break o presionamos Alt + F5 o mediante su icono, como se ve abajo. Se puede incluso usar cualquiera de los comandos de depuración como Step into, Step over, etc. Cualquier camino vale.
Con la acción indicada estamos iniciando el depurador, que más que un simulador también involucra a los verdaderos depuradores como JTAGICE3. Si en este momento hubiera conectada a la PC alguna de las tarjetas hardware como la STK600 o STK500, que incorporan interfaces de depuración, entonces el Studio 5 las detectaría y nos daría a elegir con cuál depurador deseamos trabajar. Pero como no es el caso, la única opción que nos presenta es el AVR Simulator. Para algunos AVR ni siquiera esta herramienta estará disponible. Así que la seleccionamos, y hacemos clic en OK. En el futuro ya no veremos esta ventana y la simulación correrá directamente.
Herramientas de depuración disponibles actualmente para el ATmega88P. Ahora el Studio 5 nos presentará un entorno con grandes cambios que describiremos.
Los Comandos de Depuración La barra de herramientas de depuración contiene atajos de varias de las opciones del menú Debug y es la que usaremos con mayor recurrencia. Si se llegara a perder se le puede volver sacar del menú View Toolbars Debug.
Barra de herramientas Debug con los principales comandos de depuración. Estos botones son casi estándar en todos los softwares simuladores, emuladores y depuradores. De seguro te acordarás para siempre de muchos de ellos. Lo que sí varía un poco respecto de otros programas son las teclas de atajo que los identifican. Start Debugging and Break. Inicia el modo de Simulación/Depuración. Stop Debugging. Detiene el modo de Simulación/Depuración. Step Into. Ejecuta una instrucción o sentencia del programa, pero si se trata de una llamada a una subrutina o función, se ingresará en el interior de su código. Step Over. Con este botón se ejecuta una instrucción o sentencia del programa. Si esta instrucción/sentencia es una llamada a subrutina o función, se ejecutará toda la subrutina/función de golpe. Step Out. Si en el momento actual el flujo del programa se encuentra dentro de una subrutina o función, se puede usar esta opción para salir de ella. Run to Cursor. Ejecuta el programa de corrido y se detiene en la línea donde se ubica el cursor. Para que se habilite, primero debemos colocar el cursor en un punto diferente del indicado por la flecha amarilla. Reset. Regresa la ejecución del programa al inicio del código, o mejor dicho, a la primera sentencia o instrucción disponible. Continue. Ejecuta el código del programa tan rápido como se pueda; aunque sigue estando lejos del tiempo real. A diferencia de las opciones anteriores, en este modo las ventanas no se actualizan sino hasta que paremos con un Break All o en un breakpoint. Se emplea bastante junto con los breakpoints. Break All. Detiene la evolución del programa si estamos en modo Continue. En los otros modos esta opción permanece inactiva. Show Next Statement. Es la misma flechita que dirige el flujo del programa señalando la siguiente instrucción o sentencia que se ejecutará.
Depuración en marcha del programa ledflasher3.
Las Ventanas de Depuración Estas ventanas aparecen en el menú Debug Windows solo cuando se ingresa en modo de Depuración. A continuación descubriremos la utilidad de algunas de ellas.
Ventanas del menú Debug
Windows.
La ventana Breakpoints muestra todos los breakpoints del programa, si es que existen. La forma más cómoda de poner breakpoints es haciendo doble clic en el margen izquierdo de la línea deseada. Con ello aparecerán las bolitas rojas que los representan. Los breakpoints son puntos de parada que no se dejan percibir cuando se recorre el programa paso a paso con comandos como Step in o Step over. Pero sí frenan la ejecución de la depuración cuando está en modo Run, que es iniciado por el comando Continue..
La ventana Processor presenta información de los recursos asociados con el procesador del AVR, como el Contador de Programa, el Puntero de Pila (Stack Pointer), el Registro de Estado, los Registros de Trabajo (R00 a R31) y los Punteros de RAM (X, Y y Z). Adicionalmente brinda herramientas útiles como el Contador de Ciclos y el cronómetro Stop Watch.
La ventana IO View muestra los Periféricos del AVR junto con todos sus Registros de E/S, organizados correspondientemente en dos paneles. Por ejemplo, en la siguiente figura el panel superior selecciona el módulo del puerto D y el panel inferior lista sus registros relacionados, en este caso PIND, DDRD y PORTD. Esta ventana también está disponible y ayuda muchísimo cuando se trabaja en modo de diseño. La diferencia es que en modo de depuración es posible modificar el valor de algunos de los Registros de E/S. Se podría cambiar, por ejemplo, el valor de los bits de PIND para simular el efecto de un switch o pulsador conectado a dicho pin del AVR.
Las ventanas Locals y Autos visualizan las variables de programa. Son muy parecidas. La ventana Locals muestra todas las variables de ámbito local de función actual. Por ejemplo en la siguiente figura Locals presenta las variables i, j, k, b, bi, ba, c y Effect porque actualmente se está ejecutando la función main. Por otro lado, la ventana Autos es más selectiva aún. Autos solo muestra las variables locales involucradas en la operación actual. Por ejemplo, en la figura mostrada Autos contiene a Effect porque está cerca la ejecución de la sentencia Effect = '1'; según lo señala la flecha amarilla. El campo type señala el tipo de dato de la variable, con el signo arroba @ indicando la localidad de RAM donde se ubican. El hecho de que en la figura se muestre el mensaje de "unimplemented location" significa que las variables indicadas no se encuentran en la RAM; Al examinar la ventana Regitstry (al final de esta página) comprobaremos que se hallan implementadas en los Registros de trabajo.
Las ventanas Watch (hay 4 en total) pueden desplegar cualesquiera variables del programa, sin importar a qué ámbito pertenezcan (local o global). Inicialmente Watch aparece vacía y la forma más fácil de colocar las variables en ella es seleccionar la variable en el código y arrastrarla con el mouse hasta la ventana Watch, más o menos como se ilustra en la siguiente imagen.
Las ventanas de Memoria no solo despliegan el contenido de las memorias RAM, FLASH o EEPROM del AVR, sino también de los Registros de Trabajo (R0 a R31) y de los Registros de E/S viéndolos mapeados en la RAM.
La ventana Dissassembly muestra el programa en su correspondiente código ensamblador. Es interesante ver cómo evoluciona la ejecución del programa en lenguaje C y en ensamblador en paralelo.
La ventana Registry muestra exclusivamente todos los 32 Registros de Trabajo del AVR (R00 a R31). Vemos en la siguiente figura que después de ejecutarse la sentencia Effect = '1'; aparece resaltado de rojo el valor del registro R18. Dado que los campos así resaltados corresponden a los datos que acaban de actualizarse deducimos fácilmente que la variable local Effect se almacena en el registro R18.
Medición de Tiempos con Stop Watch El Stop Watch y Cycle Counter se encuentran en la ventana Processor. Cycle Counter cuenta los ciclos de reloj transcurridos y es independiente de la frecuencia del procesador. Cada ciclo equivale a un pulso del oscilador del sistema del AVR. Cerca de la mitad de las instrucciones (las más usadas) de los AVR se ejecutan en un solo ciclo, casi todas las instrucciones que acceden a la RAM se ejecutan en dos ciclos y algunas otras como las instrucciones de salto se ejecutan en más de dos ciclos. El Stop Watch mide el tiempo de simulación transcurrido en microsegundos (us) o en milisegundos (ms). Se basa en el valor del Contador de Ciclos y la frecuencia establecida para el procesador del AVR.
Ventana Stopwatch. Observa que la frecuencia del procesador es por defecto de 1 MHz y no se actualiza al valor establecido en algún archivo de configuración. Lo primero que debemos hacer por tanto es modificarlo (si es necesario claro). Y como en casi todas las prácticas de cursomicros.com se trabaja a 8 MHz, el cambio es obligado para calibrar los tiempos correctamente. Solo selecciona el valor de Frecuency y edítalo. Ahora hagamos un ejercicio midiendo el tiempo que toma la ejecución de las sentencias puts, esto es, desde la línea 70 hasta la línea 75 del programa secuenciador de LEDs ledflasher3. Para ello sigue los siguientes pasos. Establece el flujo del programa en la primera sentencia puts (línea 70); para esto puedes colocar el cursor en esa línea y luego presionar el botón Run to Cursor
.
Ahora resetea al valor del Stop Watch y si deseas medir los ciclos transcurridos también resetea el Contador de Ciclos. Debería lucir más o menos así.
Ventana Stopwatch reseteada. Ahora aplica varios comandos Step Over para pasar todas las funciones puts; sin entrar en ellas, hasta llegar a la sentencia Effect = '1';. Otra forma de llegar a este punto es colocar allí el prompt del cursor y volver a ejecutar el comando Run to Cursor . Demorará un poco porque el simulador no es de tiempo real. Espera si desaparece la flecha amarilla antes de aplicar un comando. Pero al final nos quedará algo así:
El Stop Watch ha medido 107.144ms y el Contador de ciclos computó 857 152 ciclos.
Arquitectura Interna de los AVR ATmega
Introducción Aprender a programar microcontroladores significa aprender a usar todos sus recursos para luego aplicarlos en el diseño deseado. Es un proceso continuo, sistemático y que demanda algo de paciencia. En esta clase empezaremos por conocer el hardware interno de los AVR enfocándonos en los ATmega de las series 8yy y 4yy, que son en la actualidad los mejores microcontroladores de 8 bits de Atmel disponibles en encapsulados DIP de 28 y 40 pines respectivamente. Entre los miembros de estas familias están el ATmega48yy, ATmega88yy, ATmega168yy, ATmega328yy, ATmega164yy, ATmega324yy, ATmega644yy y ATmega1284yy. Las letras yy pueden ser P, V, PA. Existe mucha compatibilidad entre los microcontroladores de Atmel, por lo que la teoría expuesta es aplicable en gran parte a otras series de AVR como los ATtiny, los ATmega con USB o incluso a los AVR antiguos como los clásicos ATmega8535, ATmega8515, ATmega16, ATmega32, etc. A decir verdad, los AVR que estudiaremos son como las versiones mejoradas de los "clásicos" ATmega citados anteriormente. En esta clase nos familiarizaremos con la nomenclatura de los AVR, aprenderemos a reconocer sus capacidades de memoria FLASH y RAM a partir de sus nombres, y echaremos un vistazo a sus principales recursos hardware. Así que si no sabías con cuál microcontrolador AVR empezar a trabajar, éste es un buen punto de partida.
Características Comunes de los ATmega Citaremos las características más notables de los ATmegaNN8YY y ATmegaNN4YY. Quizá muchas de ellas no las comprendas de plano. Puedes tomar eso como referencia para medir tu avance en el dominio del AVR.
Tienen un repertorio de 131 instrucciones. Están optimizadas para generar un mejor código con los compiladores de alto nivel, en especial el C. Poseen 32 Registros de Trabajo de 8 bits cada uno. Se denominan desde R0 a R31. Esto en realidad es aplicable a todas las familias de los AVR de bits, incluyendo los ATxmega. Tienen una velocidad de ejecución de hasta 20 MIPS (20 Millones de Instrucciones Por Segundo), que se alcanzará cuando el reloj del sistema (XTAL) sea de 20 MHz. Aunque como en cualquier otro microcontrolador, en la práctica no será una velocidad sostenida, porque en el programa habrá instrucciones que se demoran 2 ó más ciclos de instrucción. Sin embargo, siguen siendo bastante rápidos si los comparamos por ejemplo con sus contrapartes de Microchip, los PIC18, los cuales tienen un límite de 10 o 12 MIPS. Tienen un Timer0 de 8 bits que puede trabajar como Contador o Temporizador o Generador de ondas PWM de 8 bits de resolución. Tienen un Timer1 de 16 bits que opera en modo Contador, Temporizador o como Generador de ondas PWM con resolución configurable de hasta 16 bits. Los ATmega1284yy tienen adicionalmente un Timer3 idéntico al Timer1. Tienen un Timer2 de 8 bits, parecido al Timer0 pero con soporte para operar asíncronamente con un XTAL externo. Tienen un Comparador Analógico. Tienen un módulo TWI (Two Wire Interface) para comunicaciones con el protocolo I2C en modos Maestro y Esclavo. Tienen un módulo SPI programable. Soporta los modos Maestro y Esclavo.
Tienen un Conversor ADC de 10 bits, con hasta 8 canales de entrada. Tienen un USART0: Puerto serie Transmisor Receptor Síncrono Asíncrono Universal. Los ATmegaNN4YY tienen adicionalmente un USART1, con características idénticas a las del USART0. Operan con voltajes de alimentación entre 1.8V y 5.5V. Mienrtras más alta sea la frecuencia de operación del AVR más alto será el nivel de alimentación requerido, por ejemplo, para trabajar a la máxima frecuencia de 20 MHz, Vcc debe tener un valor muy estable entre 4.5V y 5V. Tienen un Oscilador RC interno configurable como oscilador principal del sistema. Tienen 6 modos Sleep, para una mejor administración del consumo de la energía. Tienen un circuito BOD o detector de bajo voltaje de alimentación. Tienen un temporizador Watchdog, para vigilar que el programa no quede colgado. Los ATmegaNN8YY tienen 3 puertos de E/S (con 23 pines en total) y los ATmegaNN4YY tienen 4 puertos de E/S (con 32 pines en total). Oscilador del sistema seleccionable, desde el Oscilador RC interno hasta cristales de cuarzo. Tienen un modo de programación paralela de alto voltaje (14V) y un modo de programación serial en bajo voltaje (5V).
Empaques de los ATmega
Diagrama de pines de los ATmegaNN8yy en encapsulado PDIP.
Diagrama de pines de los ATmegaNN4yy en encapsulado PDIP.
Diagrama de bloques de los ATmega El siguiente diagrama muestra los principales elementos de un AVR y que tarde o temprano los tendrás que memorizar.
Diagrama de bloques simplificado de los ATmegaNN4yy / ATmegaNN8yy. Ahora una somera descripción de lo que representan estos bloques.
El CPU es el circuito encargado de leer, decodificar y ejecutar las instrucciones del programa. Dispone de 32 registros de trabajo y un ALU (Unidad Aritmético Lógica) con el que realiza las operaciones de suma, resta, AND lógica, OR lógica, etc. La Memoria FLASH, de Programa almacena las instrucciones del programa del AVR. Es una memoria permanente pero que se puede reprogramar para cambiar de tarea. La Memoria RAM, de Datos aloja las variables que procesa el CPU. El Contador de Programa es un registro que evoluciona para indicar cuál será la siguiente instrucción que debe ejecutar el CPU. La Pila o Stack es un segmento de la memoria RAM para guardar el valor del Contador de Programa y también variables temporales del programa cuando sea necesario. Los periféricos del AVR son elementos que se pueden usar para una determinada tarea; por ejemplo, el Timer0 sirve para temporizaciones. El USART para comunicaciones seriales RS232, etc. Casi todos ellos serán estudiados en un capítulo aparte. Los puertos de E/S PORTA,..., PORTD son las líneas hacia/desde el exterior donde se pueden conectar los dispositivos a controlar, como LEDs, transistores, LCDs, etc. Los ATmega de 40 pines tienen los 4 puertos completos, mientras que a los ATmega de 28 pines les falta PORTA y algunos pines en los otros puertos.
Hay más recursos presentes dentro de un AVR que también son imprescindibles pero cuyo trabajo queda en segundo plano. Algunos de ellos serán abordados en otro momento.
La Memoria de Programa
Es de tipo FLASH. Aquí es donde se aloja el programa que el CPU ejecutará. Se puede modificar por completo mediante un dispositivo programador por hasta 10 000 veces. Pero tampoco deberíamos tanto. No conozco a nadie que haya llegado a ese límite con un solo microcontrolador. Lo más probable es que, por más cuidado que tengas, llegues a freír tu AVR antes de tiempo en algún accidente. Eso es algo muy “normal”. En los AVR las instrucciones de programa son de 16 ó de 32 bits. Pero siendo la gran mayoría de 16 bits, podemos decir que un AVR de N bytes de memoria FLASH puede almacenar hasta N/2 instrucciones de código ensamblador. Antiguamente parecía sencillo reconocer la cantidad de memoria FLASH que tenían algunos AVR. Por ejemplo, un ATmega32 tiene 32 k-bytes, un ATmega8L tiene 8 k-bytes. Los ATmega de ahora todavía conservan esa correspondencia entre el número que aparece en su nombre y la cantidad de k-bytes de FLASH, solo que las nuevas series también llevan un número que puede entrar a confundir un poco. En la siguiente tabla apreciamos los ATmega de las series 8yy, 4yy, 5yy y 50yy. (Las letras yy pueden ser P, V, A o PA.) Personalmente, creo que al haber varias series, es más fácil separar primero los números que representan la capacidad de FLASH porque deben ser números redondos (digitalmente hablando), es decir, deben ser potencias de 2, como 4, 8, 16, 32, 64 ó 128. Por ejemplo, ¿cuánta memoria FLASH tendrá un ATmega3290P?, ¿3290 kbytes, 329 kbytes, 32 kbytes o 3 kbytes? Como el único número redondo es 32, la respuesta es 32 kbytes y deducimos que este ATmega es de la serie 90P. Se llega más rápido a la respuesta si leemos las opciones empezando por la izquierda. AVR ATmega48yy ATmega88yy ATmega168yy ATmega328yy ATmega164yy ATmega324yy ATmega644yy ATmega1284yy ATmega165yy ATmega325yy ATmega3250yy ATmega645yy ATmega6450yy
Memoria FLASH 4K 8K 16 K 32 K 16 K 32 K 64 K 128 K 16 K 32 K 32 K 64 K 64 K
Memoria RAM 512 1K 1K 2K 1K 2K 4K 16 K 1K 2K 2K 4K 4K
Memoria EEPROM 256 512 512 1K 512 1K 2K 4K 512 1K 1K 2K 2K
Pines de E/S 23 23 23 23 32 32 32 32 54/69 54/69 54/69 54/69 54/69
Secciones de Aplicación y de Boot Loader Los AVR ofrecen la posibilidad de escribir en su memoria de programa FLASH incluso en tiempo de ejecución. Esta función puede ser aprovechada para almacenar datos procesados por el usuario o para permitir la auto-programación del AVR. Para facilitar y robustecer el proceso de auto-programación los AVR dividen su memoria FLASH en dos segmentos lógicos, de Aplicación y de Boot loader. La Sección de Aplicación está destinada a almacenar el programa que el AVR ejecuta habitualmente, como leer sensores, controlar motores, etc.
La Sección de Boot loader está diseñada para almacenar el código del Boot loader, que es un pequeño programa para cargar el programa del AVR en la Sección de Aplicación, así como Windows carga en la RAM de la PC el programa que vamos a utilizar. Un Boot loader no es precisamente un mini S.O. porque ya hay S.O. para microcontroladores conocidos como RTOS (Real Time Operating System). Además, a diferencia de un SO para PC, un Boot loader debe ser el programa más pequeño posible y se debe ubicar al final de la memoria FLASH. No todos los ATmega tienen Sección de Boot Loader, como el ATmega48yy.
Secciones de Aplicación y de Boot Loader de la memoria de programa. La Sección de Boot loader siempre se ubica al final de la memoria FLAH pero su tamaño varía de acuerdo con el AVR y con la configuración establecida por los fuses BOOTSZ1 y BOOTSZ0. Por ejemplo, el ATmega644PA puede tener una Sección de Boot loader entre 512 palabras y 4096 palabras, según la siguiente tabla. BOOTSZ1 BOOTSZ0 1 1 0 0
1 0 1 0
Tamaño del Boot Loader 512 palabras 1024 palabras 2048 palabras 1096 palabras
Dirección del Boot Loader 0x7E00 - 0x7FFF 0x7C00 - 0x7FFF 0x7800 - 0x7FFF 0x7000 - 0x7FFF
Los fuses BOOTSZ1 y BOOTSZ0 solo se pueden modificar al grabar el AVR. Su configuración por defecto siempre establece el tamaño mayor elegible de la Sección de Boot loader. El hecho de que la Sección de Boot Loader esté diseñada para alojar el programa cargador no significa que esté limitada a esa tarea. Si no la vamos a usar para su propósito primigenio, podemos emplearla como si fuera parte de la Sección de Aplicación, o sea, como si no existiera la división entre estas dos Secciones y por ende no tendrá importancia el valor que pongamos en los fuses BOOTSZ1 y BOOTSZ0.
Configuración de los Fuses de Boot loader en el programa grabador. A propósito, la configuración de los fuses BOOTSZ1 y BOOTSZ0 en Proteus no tiene ningún efecto porque Proteus aún no soporta simulaciones con Boot Loader. Así que están como decorativos.
Configuración de los Fuses de Boot loader en Proteus.
La Memoria de Datos SRAM Actualmente suena redundante especificar memoria SRAM (Static RAM) porque casi todas las RAM de los microcontroladores son estáticas. Decimos simplemente RAM, a la memoria cuya función “tradicional” es alojar temporalmente los datos que se procesan en el programa. La cantidad de RAM disponible internamente depende del modelo de AVR y, a diferencia de la memoria FLASH, no tiene una directa correspondencia con el nombre del dispositivo. Aun así, podemos observar en la siguiente tabla que en muchos modelos existe una relación que se repite (los ATmega128nn rompen la relación, y pueden no ser los únicos). Los ATmega con 4K de FLASH tienen 512 bytes de RAM, los ATmega con 8 K y 16 K de FLASH tienen 1 K de RAM, y así, tal como se ve en la tabla. AVR ATmegaNNN
Memoria FLASH 4K
Memoria RAM
Algunos Modelos
512
ATmega48A, ATmega48PA, ATmega48P/V, ATmega48/V
AVR
Memoria FLASH
ATmegaNNN
8K
ATmegaNNN
16 K
ATmegaNNN
32 K
ATmegaNNN
64 K
ATmegaNNN
128 K
Memoria RAM
Algunos Modelos
1K
ATmega88A, ATmega88PA, ATmega88P/V, ATmega88/V ATmega168A, ATmega168PA, ATmega168P/V, ATmega168/V ATmega164A, ATmega164PA ATmega165A, ATmega165PA ATmega169, ATmega169P ATmega328, ATmega328P ATmega324A, ATmega324PA ATmega325A, ATmega325PA ATmega3250A, ATmega3250PA ATmega329P, ATmega3290P ATmega644A, ATmega644PA ATmega645A, ATmega645P ATmega6450A, ATmega6450P ATmega128, ATmega1281 ATmega1284, ATmega1284P
1K
2K
4K
4K, 8K o 16K
En los modelos listados y en general en todos los ATmega de las series más recientes el espacio de la memoria RAM (entendida en su concepto tradicional) empieza en la dirección 0x0100 y termina en 0x02FF, 0x04FF, 0x08FF, 0x10FF, 0x20FF o 0x40FF, según el modelo. La verdad, no importa mucho saber dónde termina como la cantidad misma. La dirección de inicio sí es de consideración pero cuando se programa en lenguaje ensamblador. Quizá alguien pudiera preguntar por qué la dirección de inicio no es 0x0000, como en otros microcontroladores. Porque las primeras direcciones, desde 0x0000 hasta 0x00FF, están reservadas para acceder a los Registros de Trabajo y a los Registros de E/S del AVR en „modo de memoria‟. Normalmente los 32 Registros de Trabajo se acceden directamente con instrucciones como LDI o MOV. Pero también se les puede acceder direccionándolos como si fueran parte de la memoria RAM, con instrucciones como LD o ST y con las direcciones presentadas. En ocasiones esto facilitará mover bloques de datos entre la RAM y los registros de trabajo aprovechando la potencia de los punteros. Del mismo modo, los Registros de E/S, que tampoco ocupan posiciones reales en la RAM, pueden ser accedidos como si en verdad fueran RAM con las instrucciones como LD y ST, para lo cual emplean las direcciones mostradas en la figura. Los Registros de E/S y los Registros de E/S extendidos son hermanos, por decirlo de algún modo, y tienen funciones análogas. Están separados solo por tener diferente modo de acceso, pero eso se explicará mejor en su sección respectiva. El direccionamiento y distinción de los espacios de la RAM solo son de preocupación al trabajar en ensamblador. Al programar en lenguajes de alto nivel los compiladores se encargan de toda la administración de la RAM, salvo que reciban directivas avanzadas.
Espacio de la Memoria RAM.
El Contador de Programa, PC El PC es un registro que indica la siguiente instrucción que debe ejecutar el CPU. Si vale 0x0000, ejecutará la primera instrucción de la memoria; si vale 0x0002 ejecutará la tercera instrucción, y así... Al arrancar microcontrolador, el PC vale 0x0000 y se va incrementando automáticamente, con lo que el AVR debería ejecutar una a una desde la primera hasta la última instrucción del programa. En realidad, en el código habrá instrucciones que modifiquen el valor del PC de modo que el programa nunca termine.
La Pila y el Puntero de Pila La Pila o STACK es una memoria que almacena temporalmente el valor del PC (Program Counter) cuando el programa llama a una subrutina o cuando salta a un Vector de Interrupción. También sirve para guardar datos temporalmente cuando los 32 Registros de Trabajo no sean suficientes. Al igual que en una PC, la Pila forma parte de RAM Interna. No es un pedazo de RAM con características especiales, es una simple área cuya dirección de inicio la puede establecer el usuario y cuyo tamaño es indefinido porque crece y decrece en tiempo de ejecución.
Las que sí son especiales son las instrucciones que trabajan con la Pila, como PUSH y POP. Estas instrucciones no necesitan conocer la locación en la Pila a/de donde guardarán/recuperarán los datos. Aprovechan, en cambio, su acceso de tipo LIFO (Last In First Out), que significa “el último dato en entrar será el primero en Salir”‟ Es por eso que siempre se recurre a la analogía con una pila de platos de donde no podemos tomar un plato que se encuentra en el fondo o en la mitad. Para llegar a él primero tendríamos que quitar los platos que están encima. Pero hasta ahí llega la analogía porque a diferencia de las pilas de platos, las pilas en RAM crecen de arriba abajo, es decir, cada dato que se va depositando en la Pila ocupa una dirección inferior a la anterior. Esta dirección la va marcando el Puntero de Pila, el cual se decrementa cada vez que se coloca un dato en la Pila y se incrementa cada vez que se toma un dato de ella. La Pila trabaja de cabeza para evitar que sus datos colisionen (se solapen) con las variables accedidas aleatoriamente, las cuales se van mapeando en la RAM normalmente de abajo arriba. SPHSP15SP14SP13SP12SP11SP10SP9SP8 SPLSP7SP6SP5SP4SP3SP2SP1SP0 El Puntero de Pila está representado por los registros de E/S SPH y SPL, que concatenados actúan como un registro de 16 bits. Como se dijo anteriormente, este registro tiene un comportamiento de auto-incremento y auto-decremento. La única intervención por parte del usuario debería ser su inicialización, esto es, cargarle la última dirección de la RAM. Pero esta operación solo es indispensable al programar en ensamblador. Los compiladores como el C inicializan la Pila automáticamente. Sin embargo, incluso en C es importante conocer estos conceptos para entender por ejemplo por qué un programa para un ATmega328P nunca funcionará en un ATmega168P, incluso si el tamaño del código es pequeño y le puede caber sobradamente.
Los Registros de Trabajo y los Punteros X, Y y Z Todos los AVR de 8 bits, desde los ATtiny hasta los ATxmega cuentan con 32 Registros de Trabajo nombrados desde R0 hasta R31. Los Registros de Trabajo tienen la función de alojar los datos más inmediatos que el CPU procesa. ¿Acaso ésa no era tarea de la RAM?. Bueno, sucede que en todos los microcontroladores inspirados en la arquitectura de los procesadores de Intel (como los AVR, ARM y Freescale entre otros) el acceso a la memoria RAM toma más ciclos que el acceso a los Registros de Trabajo. En los AVR de 8 bits, por ejemplo, se puede acceder a los Registros de Trabajo en un solo ciclo, puesto que todos están directamente conectados al CPU, o mejor dicho, son parte del CPU. En cambio, la mayoría de las instrucciones ensamblador que acceden a la RAM consumen 2 ciclos de instrucción. No es posible cargar datos en la RAM directamente ni moverlos entre locaciones diferentes de la RAM (a menos que tengan DMA, como los AVR32). Para esas operaciones los Registros de Trabajo actúan como intermediarios.
Pero quizá la participación más notable de Los Registros de Trabajo sea en el ALU (Unidad Aritmético Lógica) para computar las operaciones aritméticas y lógicas. Por ejemplo imaginemos que deseamos obtener la raíz cuadrada de un número de punto flotante ubicado en la RAM y almacenar el resultado de nuevo en la RAM. En lenguaje C bastaría con escribir una sola sentencia, pero el código máquina generado involucra una tarea más compleja: al inicio el CPU mueve los 4 bytes del número desde la RAM a los Registros de Trabajo, luego viene el trabajo pesado, que implica el procesamiento de varios datos intermedios. En lo posible todos estos datos también estarán en los registros de trabajo para aprovechar su velocidad y eficacia. Solo al terminar el cómputo el CPU depositará el resultado en la RAM. Los compiladores de alto nivel también suelen emplear los Registros de Trabajo para pasar los argumentos de sus funciones. Creo que con esos ejemplos debe quedar clara la razón de ser de los Registros de Trabajo. En la siguiente figura podemos notar que los Registros de Trabajo se parten por la mitad. La diferencia está en que los primeros 16 registros (R0 a R15) no admiten la instrucción LDI, que sirve para cargar constantes al registro (otro aspecto primordial de la programación en ensamblador9. Los registros R26 a R31 tienen la capacidad adicional de funcionar como punteros de 16 bits cada uno.
Los Registros de Trabajo de los AVR. El par de registros R27-R26 forma el Puntero X, el par R29-R28 forma el Puntero Y, y el par R31-R30 forma el Puntero Z.
Los punteros pueden apuntar a (contener la dirección de) cualquier locación del espacio de RAM. Esto junto con las instrucciones adecuadas conforman el direccionamiento indirecto más potente, muy útil por ejemplo para mover grandes bloques de datos.
Terminamos esta sección explicando las direcciones que figuran en el mapa de los Registros de Trabajo. En principio a los Registros de Trabajo no les debería hacer falta tener direcciones porque están directamente unidos al CPU. Hay instrucciones adecuadas como LDI y MOV para acceder a ellos. Sin embargo, los AVR les brindan direcciones para adicionalmente poder ser accedidos como si fueran parte de la RAM, es decir, con instrucciones que están diseñadas para la RAM, como LD y ST. De esta manera se hacen más flexibles las operaciones de transferencias de datos entre los diferentes espacios de memoria. Cuando manipulamos los Registros de Trabajo utilizando sus direcciones podemos decir que los estamos accediendo “en modo RAM”, pero sin perder de vista que los Registros de Trabajo no pertenecen a la RAM porque no están implementadas físicamente allí.
Los Registros de E/S Anteriormente se dijo que para programar el AVR primero había que conocer sus recursos. Pues bien, todos ellos se pueden controlar mediante los Registros de E/S. por ejemplo, si queremos manejar el puerto B, debemos conocer los registros PORTB, DDRB y PINB. Si queremos programar el USART0, debemos conocer los registros UDR0, UCSR0A, UCSR0B, UCSR0C, UBRR0L y UBRR0H. Si queremos… En tiempo de ejecución los registros de E/S (Entrada Salida) lo controlan todo, no solo las operaciones de los módulos periféricos, como se podría inferir a partir de la denominación E/S, sino que también controlan la performance del mismo CPU. En este caso los registros a conocer serían MCUCR, MCUSR o SMCR. Entenderás que no tiene caso seguir mencionando las funciones de otros Registros de E/S. es por eso que cada módulo se trata por separado estudiando con detenimiento cada registro y cada uno de los bits que lo componen.
Espero que los mapas de memoria de los Registros de E/S que presento no te hagan pensar que están divididos en una suerte de bancos, como en los PICmicro. No, señor, nada de eso. Todos los espacios de memoria en los AVR son lineales. Yo los tuve que subdividir para no presentar un “culebrón”.
Cada registro es de 8 bits y en total se cuentan 224 registros de E/S, aunque ni siquiera la mitad están implementados físicamente. Las locaciones que aparecen sin nombre están RESERVADAS y no nunca deberían ser accedidas. De modo similar, hay registros con algunos bits sin nombre ni funcionalidad que también se consideran reservados. Un ejemplo es el registro SMCR, mostrado abajo. No está prohibido acceder a dichos bits pero se recomienda dejarlos en 0 por compatibilidad con futuros AVR. Estas aclaraciones aparecen por doquier en los datasheets. Yo prefiero decirlas ahora para no tener que estar repitiéndolo a cada rato después. SMCR------------SM2SM1SM0SE Los antiguos microcontroladores AVR solo tenían el espacio de los 64 primeros Registros de E/S. A ellos les alcanzaba ese espacio aunque a veces a duras penas porque todos los registros estaban allí apretujados. Los Registros de E/S tenían sus propias instrucciones de acceso, IN y OUT, que de hecho todavía se usan. Las instrucciones de ensamblador IN y OUT utilizan el rango de direcciones 0x00 a 0x3F para acceder a los Registros de E/S. En el esquema mostrado estas direcciones aparecen fuera de los paréntesis. Con la aparición de nuevos periféricos en los AVR, aumentaron los registros de E/S y se sobrepasaba el alcance de las instrucciones IN y OUT. Así fue necesario diseñar los Registros de E/S para que también pudieran ser direccionados como si fueran parte de la RAM, o sea, con instrucciones como LD o ST, las cuales tienen mayor cobertura y permiten repotenciar las transferencias de datos. Para este tipo de acceso, llamado “en modo RAM”, las instrucciones como LD o ST utilizan el rango de direcciones 0x20 a 0xFF, que en los mapas de memoria aparecen dentro de los paréntesis. A juzgar por sus direcciones, ya podrás deducir que la única diferencia entre los Registros de E/S (estándar) y los Registros de E/S Extendidos es que estos últimos se acceden exclusivamente en “modo de memoria” con instrucciones como LD y ST, y los primeros todavía admiten el uso de las instrucciones clásicas como IN y OUT. El direccionamiento y distinción de los Registros de E/S estándar o extendidos son de especial preocupación al trabajar en ensamblador. Al programar en lenguajes de alto nivel los compiladores son quienes escogen el modo de acceso y las instrucciones que consideren más convenientes, salvo que reciban directivas contrarias.
Registros Generales del Microcontrolador
Estos son registros de E/S que no están relacionados con una parte exclusiva del microcontrolador. Por tanto los volveremos a ver al mencionar en otras clases, aunque sea para referirnos a uno solo de sus bits.
El Registro de Estado SREG Todo microprocesador tiene un registro para reflejar el estado de las operaciones lógicas y aritméticas del módulo ALU. SREG también incluye dos bits con funciones disímiles. El registro SREG es bastante utilizado en los programas en ensamblador. Cuando se trabaja con compiladores de alto nivel su presencia solo sería justificable por el uso del bit I. SREGITHSVNZC I Global Interrupt Enable 1. Habilitar las interrupciones individuales habilitadas. Las interrupciones individuales serán habilitadas en otros registros de control. 0. No se habilita ninguna de las interrupciones sin importar sin importar sus configuraciones individuales.
T
El bit I se limpia por hardware después de ocurrir una interrupción, y se setea por la instrucción para habilitar posteriores interrupciones. El bit I también se puede setear o limpiar por la aplicación con las instrucciones SEI y CLI. Bit Copy Storage
H
Las instrucciones para copiar bits (Bit LoaD) y BST (Bit STore) usan el bit T como inicio o destino para el bit de la operación. Con la instrucción BST se puede copiar un bit de un registro de trabajo al bit T y con la instrucción BLD se puede copiar el bit T a un bit de un registro de trabajo. Half Carry Flag
S
El flag H indica un medio acarreo en algunas operaciones aritméticas. El bit H es muy útil en aritmética BCD. Sign Bit, S = N ⊕ V
V
El bit S es siempre un or exclusivo entre los flags N y V. Leer abajo. Two‟s Complement Overflow Flag
N
El flag V soporta aritmética de complemento a dos. Negative Flag
Z
El flag N indica un resultado negativo en una operación lógica o aritmética. Zero Flag
C
El flag Z indica un resultado cero en una operación lógica o aritmética. Carry Flag El flag C indica un acarreo en una operación lógica o aritmética.
El Registro MCUCR
MCUCR = MCU Control Register. MCU a su vez significa Micro Controller Unit. Aunque probablemente de este registro solo vayamos a usar el bit PUD, es bueno conocerlo ahora aprovechando que hace referencia a muchas de las características del microcontrolador que se estudiaron en esta clase. Ampliaremos la funcionalidad del bit PUD en la clase de puertos y entrada y salida generales y los bits IVSEL junto con IVCE serán mejor expuestos en la clase de interrupciones. MCUCRJTDBODSBODSEPUD------IVSELIVCE JTD JTAG Interface Disable 0. La interface JTAG estará habilitada si está programado el fuse JTAGEN. 1. la interface JTAG está deshabilitada.
BODS
Para evitar deshabilitaciones no intencionadas de la interface JTAG, se debe seguir una secuencia especial para cambiar este bit: el software de aplicación debe escribir este bit dos veces el valor deseado dentro de cuatro ciclos de reloj. Este bit no se debe alterar cuando se esté usando el sistema OCD (On Chip Debug) BOD Sleep Sirve para deshabilitar el circuito del BOD durante el modo Sleep, si estaba activo. Recuerda que se activa por los fuses BODLEVEL2-0. Para poner 1 en el bit BODS se requiere seguir una secuencia especial que involucra el bit BODSE. Primero se escribe 1 en los bits BODS y BODSE. Luego se debe escribir 1 en el bit BODS y 0 en el bit BODSE dentro de los siguientes cuatro ciclos de reloj.
BODSE
El bit BODS estará activo durante tres ciclos de reloj después de haberse seteado. Para apagar el circuito BOD para el modo Sleep actual se debe ejecutar la instrucción Sleep mientras el bit BODS esté activo. El bit BODS se limpia automáticamente después de tres ciclos de reloj. BOD Sleep Enable
PUD
El bit BODSE habilita la configuración del bit de control BODS, como se explicó en la descripción del bit BODS. Pull-up Disable 1. Se deshabilitan las pull-up de todos los pines de los puertos, sin importar el valor de los registros DDRx y PORTx.
IVSEL
0. Las pull-up se habilitaran por los registros DDRx y PORTx. Interrupt Vector Select 0. Los Vectores de Interrupción se ubican en el inicio de la memoria FLASH. 1. Los Vectores de Interrupción se mueven al inicio de la Sección de Boot Loader de la memoria FLASH. Para evitar cambios no intencionados en este bit se debe seguir una secuencia
especial: a. Escribir 1 en el bit IVCE. b. Dentro de los cuatro ciclos de reloj siguiente, escribir el valor deseado en el bit IVSEL mientras se escribe un 0 en el bit IVCE. Las interrupciones se deshabilitarán automáticamente durante la ejecución de esta secuencia. Las interrupciones se deshabilitan en el ciclo en que se setea el bit IVCE y permanecen deshabilitadas hasta después de la instrucción que escribe el bit IVSEL. Si no se escribe el bit IVSEL, las interrupciones permanecen deshabilitadas por cuatro ciclos de reloj. El bit I en el registro de estado SREG no se afecta por la deshabilitación automática.
IVCE
Nota: si los Vectores de Interrupción están colocados en la Sección de Boot Loader, y el bit de candado BLB02 está programado, las interrupciones se deshabilitan durante la ejecución del programa desde la Sección de Boot Loader. Interrupt Vector Change Enable Se debe escribir 1 en el bit IVCE para habilitar el cambio del bit IVSEL. El bit IVCE se limpia por hardware cuatro ciclos después de que se haya escrito o cuando se escribe el bit IVSEL. Setear el bit IVCE deshabilitará las interrupciones, como se explicó en la descripción del bit IVSEL.
El Registro MCUSR MCUSR = MCU Status Register. Es el registro de estado del microcontrolador, MCU. MCUSR está conformado por bits de Flag que sirven para identificar la causa del reset del microcontrolador. Para averiguar la fuente de reset el programa debe leer el registro MCUSR tan pronto como sea posible y luego limpiar sus Flags. Observa que a diferencia de los Flags de las Interrupciones, los Flags de MCUSR se limpian escribiendo un cero y no un uno. MCUSR---------JTRFWDRFBORFEXTRFPORF JTRF JTAG Reset Flag
WDRF
Este bit se pone a uno si se produce un reset por un uno lógico en el Registro JTAG Reset seleccionado por la instrucción de JTAG AVR_RESET. Este bit se pone a cero por un Reset POR, o escribiendo un cero lógico en el flag. Watchdog Reset Flag
BORF
Este bit se pone a uno cuando se produce un Reset por el Watchdog. Este bit se pone a cero por un reset Power-on o escribiendo un cero lógico en el flag. Brown-out Reset Flag Este bit se pone a uno cuando se produce un Reset Brown-out. Este bit se pone a cero por un reset Power-on o escribiendo un cero lógico en el flag.
EXTRF
PORF
External Reset Flag Este bit se pone a uno cuando se produce un Reset Externo. Este bit se pone a cero por un reset Power-on o escribiendo un cero lógico en el flag. Power-on Reset Flag Este bit se pone a uno cuando se produce un Reset Power-on. Este bit se pone a cero escribiendo un cero lógico en el flag.
Los Fuses de los AVR Los fuses del microcontrolador establecen una característica importante en su operación, tanto que solo se puede modificar en el momento de programarlo. Por ejemplo, no se podría escoger el tipo de oscilador a usar después de haber iniciado el programa. Sería como cambiarle los neumáticos a un automóvil en marcha. Cada aplicación puede requerir una configuración particular y si no se establecen los fuses correctos, el programa puede funcionar mal, suponiendo que funcione. :) A diferencia de los PICmicro, en los AVR los fuses no pueden formar parte del código HEX, así que siempre será necesario configurarlos directamente en el entorno del programa grabador de AVR. Los fuses sí pueden incluirse en los archivos de depuración como ELF, para simulaciones o depuraciones. Por otra parte, y esto es para bien, hay muy poca variación en los fuses de los AVR, inclusive de distintas series. Por eso serán de fácil recordación, en contraste con los fuses de los PICmicro, donde son casi inmemorizables. La comparación es siempre con los PIC18, por supuesto, porque los PIC16 en general no están a la altura. Los fuses están contenidos en los denominados Bytes de Fuses, que son registros (de 8 bits obviamente) implementados en EEPROM (no en la EEPROM de datos de AVR). Por eso un ‟1‟ es el valor por defecto cuando un bit no está programado. Los ATmega más recientes tienen 3 Bytes de Fuses llamados Byte de Fuses Bajo, Alto y Extendido. El Byte de Fuses Bajo es el mismo en todos ellos. Los otros empiezan a cambiar un poco, en orden más que en funciones, según cada modelo en particular. Mientras más reciente sea la serie, se encontrarán menos divergencias. El conjunto de Bytes de Fuses mostrado a continuación pertenece a los AVR de la serie ATmegaNN4YY. En otras series pueden aparecer, desaparecer o cambiar algunos fuses, por ejemplo los AVR de la serie ATmegaNN5YY tienen adicionalmente el fuse RSTDISBL. En lo sucesivo se describirán las funciones de los fuses más conocidos, no solo los presentados aquí. Byte de Fuses Bajo CKDIV8 CKOUT SUT1 SUT0 CKSEL3 CKSEL2 CKSEL1
Bit 7 6 5 4 3 2 1
Descripción Divide clock by 8 Clock output Select start-up time Select start-up time Select Clock source Select Clock source Select Clock source
Valor por Defecto 0 (programado) 1 (sin programar) 1 (sin programar) 0 (programado) 0 (programado) 0 (programado) 1 (sin programar)
Byte de Fuses Bajo CKSEL0 Byte de Fuses Alto OCDEN JTAGEN SPIEN WDTON EESAVE BOOTSZ1 BOOTSZ0 BOOTRST Byte de Fuses
Bit 0 Bit 7 6 5 4 3 2 1 0 Bit
Extendido – – – – – BODLEVEL2 BODLEVEL1 BODLEVEL0
7 6 5 4 3 2 1 0
Descripción Select Clock source Descripción Enable OCD Enable JTAG Enable Serial Programming Watchdog Timer always on EEPROM memory is preserved Select Boot Size Select Boot Size Select Reset Vector Descripción – – – – – Brown-out Detector trigger level Brown-out Detector trigger level Brown-out Detector trigger level
Valor por Defecto 0 (programado) Valor por Defecto 1 (sin programar) 0 (programado) 0 (programado) 1 (sin programar) 1 (sin programar) 0 (programado) 0 (programado) 1 (sin programar) Valor por Defecto 1 1 1 1 1 1 (sin programar) 1 (sin programar) 1 (sin programar)
Pongo en relieve estos detalles porque hay muchos programadores, como el famoso WinPic800, en los que los fuses se configuran bit a bit, como se ve abajo. ¿Cómo harías si tuvieras que configurar el uso de un oscilador de XTAL de 16 MHz en un ATmega3250P? Primero tendrías que conocer los fuses correspondientes, ¿cierto? En este caso son CKSEL0, CKSEL1, CKSEL2 y CKSEL3. Luego tendrías que descubrir cuál es la combinación correcta para un XTAL de 16MHz. No te preocupes, todo esto lo estudiaremos ampliamente.
Configuración de los fuses en WinPic800. WinPic800 es bueno pero sus engreídos son los PICmicro y a los AVR no les da tanta consideración como a ellos (debe ser por su nombre ;). También existen programadores con software de mejor interface como el AVRprog2 con su software AVRFLASH, cuyo entorno se muestra en abajo. A propósito, la toma indica que hay opciones para usar un XTAL mayor de 8MHz. Podrías elegir cualquiera, a menos que sepas lo que significan arranque rápido o lento, y estés seguro de que necesitas uno de ellos. Bueno, mejor vamos a la descripción de los fuses de una vez.
Configuración de los fuses en AVRFLASH.
CKSEL3-0. Selección del Reloj Este fuse se representa por los bits CKSEL3, CKSEL2, CKSEL1 y CKSEL0. Sirve para adaptar el circuito interno del oscilador según el componente externo o interno que se usará como fuente del reloj del sistema.
Reloj Externo. En este caso la fuente de reloj será una señal de onda cuadrada externa aplicada al pin XTAL1 del AVR. Su frecuencia podrá estar en todo el rango posible, desde 0 hasta 20 MHz. Se establece con los bits CKSEL3-0 = 0000, respectivamente. Oscilador RC Interno Calibrado. Con esta opción no se necesitarán añadir componentes externos. El reloj será el oscilador RC Interno, el cual tiene una frecuencia cercana a 8MHz, y adicionalmente ofrece al diseñador la posibilidad de ajustar por software dicha calibración hasta en 1%. Es útil para sistemas de bajo costo aunque de menor nivel de estabilidad. Es la opción por defecto. Se establece poniendo los bits CKSEL3-0 = 0010, respectivamente.
XTAL Externo de Baja Frecuencia. Para utilizar un XTAL externo de 32.768 kHz con capacitores de estabilización opcionales. Es una configuración diseñada para aplicaciones de reloj. En los ATmega 8xx y 4xx se establece poniendo los bits CKSEL3-0 = 0100 o 0101. En otros ATmega puede variar. Oscilador RC Interno de 128kHz. Este oscilador es parecido al RC de 8MHz, solo que ofrece menor precisión y no se puede calibrar. En los ATmega 8xx y 4xx se establece con los bits CKSEL3-0 iguales a 0011. En otros ATmega puede no estar disponible. XTAL Externo de Baja Potencia. Esta configuración soporta cristales de 0.9 MHz hasta 16 MHz. Estos cristales ahorran energía pero su uso es recomendado solo en ambientes libres de ruido. El XTAL se coloca entre los pines XTAL1 y XTAL2 del AVR y debe usar capacitores externos de entre 12pf y 22pf, similar al circuito con XTAL estándar de alta frecuencia. En los ATmega 8xx y 4xx los valores de los bits CKSEL3-0 dependerán del rango de frecuencia del XTAL, según la siguiente tabla. Rango del XTAL en MHz 0.9 - 3.0 3.0 - 8.0 8.0 - 16.0
CKSEL3-0 1011 o 1010 1101 o 1100 1111 o 1110
XTAL Externo Estándar. Con esta configuración el XTAL usado estará entre 0.4 MHz y 20 MHz (el máximo admisible). El XTAL se coloca entre los pines XTAL1 y XTAL2 del AVR y debe tener capacitores externos de entre 12pF y 22pF. En los ATmega 8xx y 4xx se establece con los bits CKSEL3-0 = 0111 o 0110, dependiendo de la frecuencia del cristal usado, segun la siguiente tabla.. Rango del XTAL en MHz 0.4 - 16 0.4 - 20
CKSEL3-0 0111 o 0110 0111
Observa que con los 4 bits CKSEL3-0 es posible formar hasta 16 combinaciones. Entre las no citadas, algunas están reservadas y no deberían usarse, y otras corresponden a configuraciones relacionadas con el uso de un Resonador Cerámico en lugar del XTAL externo estándar o de baja potencia. Excluí las combinaciones del resonador cerámico para evitar una sobrecarga de números innecesaria. En la mayoría de las prácticas de cursomicros.com se usa un XTAL de 8 MHz. Puede ser estándar o de baja potencia. El XTAL de cuarzo brinda el reloj más preciso y estable, lo que es ideal para aplicaciones que usan los módulos síncronos como el USART o TWI (I2C). Debemos recordar que un 0 es un bit programado y un 1 es un bit sin programar. En los entornos de los programadores 0 suele ser una casilla marcada. Por ejemplo, en WinPic800 la combinación CKSEL3-0 = 0111 se vería así.
SUT1-0. Retardos de Reset y de Arranque Se representa por los bits SUT1 y SUT0. En realidad se trata de dos temporizadores. El Retardo de Reset funciona con el circuito RC del Watchdog y se encarga de mantener el AVR en estado de reset por 4.1 ms o 65 ms después de un RESET, por ejemplo, después de conectar la alimentación Vcc del AVR. Esto serviría para que se estabilice el nivel de Vcc antes de que el AVR empiece a trabajar. Se representa por .
Pero ahí no termina. Para asegurarse de que el oscilador del sistema también se haya estabilizado, habrá un Retardo de Arranque hasta que transcurran 1K o 16K ciclos de reloj CK. Durante ese lapso el AVR también se mantiene en estado de reset y luego recién el procesador empezará a ejecutar el programa. El Retardo de Arranque no solo se activa después de un reset sino también después de que el AVR salga de los estados Power-save o Power-down, que son dos de los seis modos SLEEP que tienen los ATmega. La configuraciones de los retardos de reset y de arranque varían de acuerdo con la fuente de reloj del sistema. La siguiente tabla muestra solo las opciones admisibles para un XTAL estándar o de baja potencia. CKSEL0 0 0 1 1 1 1
SUT1 SUT0 10 11 00 01 10 11
Retardo de arranque 14CK 1K CK 4.1ms + 14CK 1K CK 65ms + 14CK 1K CK 14CK 16K CK 4.1ms + 14CK 16K CK 65ms + 14CK 16K CK
Retardo de reset
Fuente de Oscilador, Condiciones de alimentación XTAL baja frecuencia, BOD enabled XTAL baja frecuencia, fast rising power XTAL baja frecuencia, slowly rising power XTAL, BOD enabled XTAL, fast rising power XTAL, slowly rising power
En esta tabla XTAL baja frecuencia no se refiere al XTAL de reloj de 32.768 kHz, sino a un XTAL estándar o de baja potencia cuya frecuencia no esté cercana a la máxima admisible por el AVR, que para los ATmega 8xx o 4xx es de 20MHz. Esta condición concuerda con el valor del bit CKSEL0 dado en las tablas de los bits CKSEL3-0 vistas anteriormente. Como se ve algo confuso, vamos a poner un ejemplo de diseño. Supongamos que nuestra aplicación utilizará un ATmega644P a una frecuencia muy estable de 20 MHz. En primer lugar debemos utilizar un XTAL estándar de 20MHz, para lo cual debemos programar los bits CKSEL3-0 con 0111 (ver las tablas de arriba). Como el bit CKSEL0 en este caso es 1, descartamos las configuraciones de la tabla donde CKSEL0 es 0. Así mismo, como 20MHz no es una frecuencia baja, también descartamos la configuración de XTAL baja frecuencia, slowly rising power. Ahora todavía tenemos las tres opciones resaltadas en la siguiente tabla. CKSEL0 0 0 1 1 1 1
SUT1 SUT0 10 11 00 01 10 11
Retardo de arranque 14CK 1K CK 4.1ms + 14CK 1K CK 65ms + 14CK 1K CK 14CK 16K CK 4.1ms + 14CK 16K CK 65ms + 14CK 16K CK
Retardo de reset
Fuente de Oscilador, Condiciones de alimentación XTAL baja frecuencia, BOD enabled XTAL baja frecuencia, fast rising power XTAL baja frecuencia, slowly rising power XTAL, BOD enabled XTAL, fast rising power XTAL, slowly rising power
¿Cuál elegir? Vemos que los ciclos de reloj son todos iguales a 16K CK. Entonces nuestra decisión dependerá del retardo de reset requerido. Si nuestro circuito tiene una alimentación Vcc que se eleva lentamente (slowly rising power), “debemos” elegir un retardo de 65ms con SUT1-0 = 11. Pero si en nuestro circuito el nivel de Vcc se eleva rápidamente (fast rising power), podemos optar por el retardo de 4.1ms con SUT1-0 = 10. Por supuesto que también en este caso podemos optar por el retardo de 65ms. Por último, explicamos lo que significa XTAL, BOD enabled. BOD es un circuito interno que detecta cuando la tensión de Vcc cae debajo de ciertos niveles. También sirve para que el AVR no esté trabajando con una alimentación defectuosa. Así que configurar el retardo de 14 CK (lo mismo que nada) equivale a no utilizar
retardo de reset y solo debe utilizarse cuando nuestra aplicación trabaje con un circuito BOD externo o interno. El BOD interno del AVR se configura con los bits BODLEVEL2-0.
CKDIV8. Prescaler del Reloj del Sistema Los ATmega recientes, como los tratados en esta clase, tienen un prescaler en el sistema de reloj, que dividirá la frecuencia del reloj cualquiera que sea su fuente (XTAL, RC Interno etc.). El Prescaler puede dividir la frecuencia del reloj entre 256, entre 128, entre 64,… hasta 1, dependiendo del valor que se cargue en el Registro de E/S CLKPR, el cual se puede modificar en cualquier momento en tiempo de ejecución. En este momento basta con saber que el bit CKDIV8 configura el registro CLKPR para que el reloj se divida inicialmente entre 8 o entre 1. CKDIV8 Factor del Prescaler del Reloj 0 8 1 1 El valor por defecto del bit CKDIV8 es 0 = programado = reloj dividido entre 8. Si lo dejamos así, nuestro ATmega operará 8 veces más lento. Es más sencillo desmarcar esa casilla que escribir el código que reconfigura el Prescaler. A propósito, el Wizard de CodeVision AVR genera ese código automáticamente.
BODLEVEL2-0. Voltaje de Disparo del BOD Es un reset por baja tensión. Esta característica le permite al AVR auto resetearse cada vez que detecte una caída de tensión en la alimentación Vcc, por debajo de un nivel establecido por los bits BODLEVEL2, BODLEVEL1 y BODLEVEL0. En la figura subsiguiente la señal de RESET INTERNO se activa cuando el nivel de Vcc cae por debajo de VBOT-. Luego para que el procesador del AVR vuelva a ejecutar el programa no solo bastará que Vcc supere el valor de VBOT+, sino que transcurra el RETARDO DE RESET . La diferencia entre VBOT+ y VBOTconstituye el mecanismo de histéresis. El AVR tiene un filtro pasa-bajas interno para evadir el ruido y evitar un reset ante los microvalles de tensión en Vcc. La caída de tensión tiene que ser mayor a 2us (valor típico). BODLEVEL2-0 111 110 101 100
Voltaje de disparo de BOD (valor típico) BOD Desactivado 1.8 2.7 4.3
WDTON. Habilitación del Watchdog El Watchdog o WDT es un temporizador que puede monitorizar el funcionamiento fluido del microcontrolador. El WDT lo estudiaremos al final porque no es imprescindible. De momento diremos que se trata de un temporizador que una vez habilitado debemos resetear periódicamente en el programa. Si no lo hacemos, causará un reset en el AVR y el programa se volverá a ejecutar desde cero. Cuando el WDT no está habilitado por su fuse WDTON (hardware) todavía es posible activarlo por software. Pero una vez activado por su fuse no habrá rutina software que lo apague. En la mayoría de las prácticas no querremos estar preocupándonos del estado del WDT, así que la mejor decisión será tenerlo inhabilitarlo, que felizmente es el valor por defecto del bit WDTON, así que ni siquiera tendríamos que tocarlo.
CKOUT. Salida de Reloj Cuando este fuse está programado la señal de reloj del sistema saldrá por el pin CLKO de los ATmega (ver más abajo). Esta señal incluye el efecto del Prescaler de reloj. Puede servir para sincronizar el microcontrolador con otros dispositivos de la aplicación, pero se usa muy raramente, así que lo habitual es dejar CKOUT con su valor por defecto 1, o sea sin programar. Si se programa accidentalmente y el diseño utiliza el pin CLKO para una función de E/S, se puede producir un corto circuito que dañe el pin o hasta el ATmega completo.
OCDEN y JTAGEN. Depuración OCD y JTAG Con los módulos OCD o JTAG habilitados, los ATmega que los tienen ponen en acción la circuitería interna que monitoriza el estado del CPU, de los periféricos internos, de todos los registros del AVR y de las memorias RAM, FLASH y EEPROM. Los resultados serán enviados a un ordenador a través de la interface conformada por los pines TMS, TCK, TDI y TDO (ver más abajo). Del lado del ordenador estará corriendo un programa como el Studio 5 en modo JTAGICE para recibir todos los datos e ir visualizándolos en la pantalla. También es posible enviar desde el ordenador comandos de ejecución del programa como Step into, Step over, etc. En otras palabras, es como correr el simulador del Studio 5 o VSM Proteus, pero esto será real y a veces en tiempo real. Además la interface JTAG también permite reprogramar el AVR. El valor predeterminado de OCDEN es desactivado pero el de JTAGEN es activado. Si no vamos a usar el sistema OCD ni la interface JTAG debemos desactivarlos para que sus circuitos no consuman recursos del sistema innecesariamente y para no renunciar a los pines TMS, TCK, TDI y TDO como puertos de E/S generales.
DWEN. Habilitar Línea de Depuración No todos los ATmega tienen un sistema de depuración sofisticado con interface JTAG. Otros tienen un sistema OCD con una interface sencilla conformada por una sola línea, en este caso por el pin DW, que es el mismísimo pin de RESET del ATmega, aunque normalmente no se grafica así en sus diagramas de pines. Su configuración predeterminada es deshabilitada y debemos dejarla así si no lo vamos a usar. De lo contrario el circuito de monitoreo del OCD interno también estará funcionando activamente, consumiendo energía innecesaria.
EESAVE. Preservar Contenido de la EEPROM Cada vez que grabamos nuestro AVR con un nuevo programa se ejecutará previamente un borrado completo de la memoria FLASH y de la EEPROM interna. Si queremos que la EEPROM no se borre en este proceso debemos programar este fuse. Su configuración predeterminada es sin programar, como se muestra abajo.
SPIEN. Habilitación de Programación Serial Los ATmega tienen dos modos de programación. Programación paralela de alto voltaje (a 14 V) y programación serial de bajo voltaje (a 5 V). La programación paralela siempre estará disponible pero la programación serial se puede deshabilitar mediante el fuse SPIEN. La modificación del bit SPIEN solo es posible desde la programación paralela. Los ATmega vienen con su programación serial activada de fábrica, como se aprecia en la siguiente imagen.
BOOTSZ1-0. Tamaño de la Sección de Boot Loader Este fuse configura el tamaño que tendrá la Sección de Boot Loader, estudiada previamente. Si no vamos a usar esta característica del ATmega, no interesan los valores que tengan los bits BOOSZ1 y BOOTSZ0.
BOOTRST. Ubicación del Vector de Reset El Vector de Reset es la dirección de la memoria FLASH por donde se empezará a ejecutar el programa. Normalmente es 0x0000 porque el código del programa empieza a mapearse desde la primera dirección. La única situación en que esto debe cambiar es cuando se usa un programa de Boot Loader. Cuando el fuse BOOTRST está activado el vector de reset será la primera dirección de la Sección de Boot Loader. Como es de esperar, la configuración por defecto de este fuse es sin programar, como se ve abajo, y no debería modificarse a menos que se sepa bien lo que se hace.
RSTDISBL. Deshabilitar Reset Externo Está disponible en muchos ATmega de 28 pines. Por defecto este fuse no está programado y el pin de RESET cumple su función habitual de reiniciar la ejecución del programa cuando se pone a nivel bajo.
Pero si programamos el bit RSTDISBL, el pin reset trabajará como PC6, o sea, como el séptimo pin de E/S del puerto C. Yo no suelo programar este bit porque me gusta resetear el AVR a cada rato para asegurarme de que programa siempre inicia bien.
SELFPRGEN. Habilitar Autoprogramación
Este fuse solo está disponible en los ATmega que no dividen su memoria en secciones de Aplicación y de Boot Loader, es decir, en los AVR que no soportan el Boot Loader convencional. Con el fuse programado se podrá utilizar la instrucción de ensamblador SPM en cualquier parte del programa. En caso contrario SPM no tendrá efecto. Por defecto el fuse SELFPRGEN está sin programar. Ya sea que se trabaje en C o en ensamblador, el programador normalmente sabe si va a acceder a la memoria FLASH para escritura. En ese caso se deberá activar este fuse. Y si no tienes idea de lo que estoy hablando, casi te puedo asegurar que no interesa el valor que le des a este fuse y hasta te recomendaría que lo actives.
Lock Bits o Bits de Candado “Last, not least”. De hecho, los Bits de Candado son más importantes que los fuses. Por eso aparecen en primera posición en los softwares de grabación. Al menos los fuses se pueden reprogramar, en cambio los Bits de Candado podrían establecer una configuración definitiva que deje al AVR inutilizable. Todos los ATmega disponen de los Bits de candado generales LB1 y LB2 y, obviamente, los bits de candado de Boot Loader solo están presentes en los ATmega con soporte de Boot Loader.
Byte de Bits de Lock
BLB12 BLB11 BLB02 BLB01 LB2 LB1
Bit 7 6 5 4 3 2 1 0
Descripción – – Boot Lock bit (Bit de candado de Boot Loader) Boot Lock bit (Bit de candado de Boot Loader) Boot Lock bit (Bit de candado de Boot Loader) Boot Lock bit (Bit de candado de Boot Loader) Lock bit (Bit de candado general) Lock Bit (Bit de candado general)
Valor por defecto 1 (sin programar) 1 (sin programar) 1 (sin programar) 1 (sin programar) 1 (sin programar) 1 (sin programar) 1 (sin programar) 1 (sin programar)
Los bits de candado generales LB1 y LB2 establecen tres modos de protección que se describen en la tabla de abajo. Debes tener especial cuidado con los modos 2 y 3. Si eliges el modo 3 tu AVR se quedará con su programa actual y no podrás volver a programarlo. Con esta opción tampoco se podrá leer el código de programa del AVR desde un programador. Esto podría servir como una “protección de código”, pero sin marcha atrás. En el modo 2 todavía será posible reprogramar el AVR, pero solo mediante la interface JTAG. Con esta interface también se pueden programar los bits de fuses y de candado. Modo de LB 1 2
3
LB2 LB1 Tipo de protección 11 No se habilita ninguna característica de Candado. Se deshabilitan las posteriores programaciones de las memorias FLASH y 10 EEPROM tanto en modo de programación Serial y Paralela. También se bloquean los bits de los fuses. Se deshabilitan las posteriores programaciones y verificaciones de las memorias FLASH y EEPROM tanto en modo de programación Serial, Paralela 00 y JTAG (si se tiene). También se bloquean los bits de los Fuses y los bits de candado de Boot Loader en los modos de programación Serial y Paralela.
Para comprender el uso de los 4 bits de candado de Boot Loader debemos conocer primero la función de las Secciones de Aplicación y de Boot Loader, además de las instrucciones de ensamblador LPM y SPM, que participan activamente en la auto-programación del AVR. LPM (Load from Program Memory) sirve para leer un byte de dato de la memoria FLASH. SPM (Store to Program Memory) sirve para escribir un byte de dato en la memoria FLASH. Ambas instrucciones, LPM y SPM, trabajan con el puntero Z para direccionar la memoria FLASH. Modo de BLB0 1 2
3
BLB02 BLB01 Tipo de protección No habrá restricciones para SPM o LPM en su acceso a la Sección de 11 Aplicación. 10 No se permite el uso de SPM para escribir en la Sección de Aplicación. No se permite el uso de SPM para escribir en la Sección de Aplicación, ni el uso de LPM para leer la Sección de Aplicación desde la Sección de Boot Loader. 00 Si los Vectores de Interrupción están ubicados en la Sección de Boot Loader, las interrupciones se deshabilitan durante la ejecución desde la Sección de Aplicación.
Modo de BLB0
4
Modo de BLB1 1 2
3
4
BLB02 BLB01 Tipo de protección No se permite el uso de LPM para leer la Sección de Aplicación desde la Sección de Boot Loader. 01 Si los Vectores de Interrupción están ubicados en la Sección de Boot Loader, las interrupciones se deshabilitan durante la ejecución desde la Sección de Aplicación. BLB12 BLB11 Tipo de protección No habrá restricciones para SPM o LPM en su acceso a la Sección de 11 Boot Loader. No se permite el uso de SPM para escribir en la Sección de Boot 10 Loader. No se permite el uso de SPM para escribir en la Sección de Boot Loader, ni el uso de LPM para leer la Sección de Boot Loader desde la Sección de Aplicación. 00 Si los Vectores de Interrupción están ubicados en la Sección de Aplicación, las interrupciones se deshabilitan durante la ejecución desde la Sección de Boot Loader. No se permite el uso de LPM para leer la Sección de Boot Loader desde la Sección de Aplicación. 01 Si los Vectores de Interrupción están ubicados en la Sección de Aplicación, las interrupciones se deshabilitan durante la ejecución desde la Sección de Boot Loader.
El valor predeterminado de todos bits de candado es 1 ó sin programar. En los softwares de programación como WinPic800 significa que las casillas respectivas están desmarcadas y así se deberían dejar. En otros software como AVRFLASH se indican las configuraciones por sus modos (1, 2, 3 ó 4) y siendo los modos 1 los predeterminados, también deberían quedar así, a menos que estemos seguros de lo que hacemos.
Entrada y Salida Generales
Los Puertos de los AVR Los puertos se conforman por las líneas del microcontrolador donde se pueden conectar los dispositivos de Entrada/Salida a controlar, por ejemplo LEDs, displays, transistores, otros ICs o, mediante relés u optoacopladores, cargas de 110V/220V como medianos motores. Los ATmegaNN4YY tienen 4 puertos, llamados PORTA, PORTB, PORTC y PORTD. Todos ellos están completos, así que disponen de 4 x 8 = 32 pines en total. Los ATmegaNN8YY tienen 3 puertos, PORTB, PORTC y PORTD. En total suman 20 pines con capacidad de ampliarse a 23 pines. Los pines de los puertos tienen nombres compuestos, como PC4 ADC4/SDA/PCINT12. Los nombres compuestos implican que tienen funciones multiplexadas. Por ejemplo, este pin PC4, además de pin digital convencional puede funcionar como el canal 4 del conversor analógico digital ADC4, como línea serial de datos SDA del módulo TWI (I2C) o como pin de interrupción por cambio de estado PCINT12. Cuando los pines no son interface de algún periférico, como el USART, el ADC, los Timers, etc., es decir, cuando son manual y directamente controlados por sus registros PORTx, PINx y DDRx se dice que actúan como Entradas y Salidas Generales, y es a lo que nos dedicaremos en esta clase. En principio todos los pines son bidireccionales cuando actúan como líneas de Entrada y Salida Generales. La dirección es configurable por software. Algunos pines pierden esa función cuando su control es asumido por algún módulo relacionado, por ejemplo, una vez configurado el puerto serie, el USART asumirá el control de los pines TXD y RXD.
Puertos de los ATmegaNN4YY.
Puertos de los ATmegaNN8YY.
Capacidades de Voltaje y Corriente
Cuando actúan como salidas, los pines pueden entregar tensiones de hasta Vcc. Cuando actúan como entradas pueden manejar niveles de hasta 0.5V por encima de Vcc. El diseño de los pines incluye diodos internos de sujeción que les permiten soportar tensiones mucho mayores que Vcc o inferiores que GND, siempre que la corriente no sobrepase del orden de los micro Amperios. Cada pin de E/S puede soportar picos de corriente de hasta 40 mA, pero en estado estable cada pin de puerto puede suministrar o recibir hasta 20 mA de corriente cuando Vcc = 5V y hasta 10 mA cuando Vcc = 3V. Sin embargo, esta capacidad no puede estar presente en todos los pines al mismo tiempo. En seguida tenemos los límites de corriente total que soportan los puertos en los ATmegaNN4YY:
La suma de las corrientes suministradas por lo pines PB0 - PB7, PD0 - PD7 y XTAL2 no debería exceder los 100 mA. La suma de las corrientes suministradas por los pines PA0 - PA3 y PC0 - PC7 no debería exceder los 100 mA. La suma de las corrientes recibidas por los pines PB0 - PB7, PD0 - PD7 y XTAL2 no debería exceder los 100 mA. La suma de las corrientes recibidas por los pines PA0 - PA3 y PC0 - PC7 no debería exceder los 100 mA.
Para los ATmegaNN8YY la distribución de límites es un poco diferente. (Los pines ADC7 y ADC6 no están presentes en encapsulados DIP.)
La suma de las corrientes suministradas por lo pines PC0 - PC5, ADC7, ADC6 no debería exceder los 100 mA. La suma de las corrientes suministradas por los pines PB0 - PB5, PD5 - PD7, XTAL1 y XTAL2 no debería exceder los 100 mA. La suma de las corrientes suministradas por los pines PD0 - PD4 y RESET no debería exceder los 100 mA. La suma de las corrientes recibidas por los pines PC0 - PC5, PD0 - PD4, ADC7 y RESET no debería exceder los 150 mA. La suma de las corrientes recibidas por los pines PB0 - PB5, PD5 - PD7, ADC6, XTAL1 y XTAL2 no debería exceder los 150 mA.
Las Resistencias de Pull-up Una de las cualidades que distinguen a los microcontroladores de los microprocesadores es que encierran en un solo chip todos los elementos posibles de un sistema de control. Con este fin los AVR incorporan en todos sus puertos transistores a manera de fuente de corriente que en la práctica funcionan como resistencias de pullup. Estas pull-ups nos pueden ahorrar el uso resistencias de sujeción externas en los pines de los puertos configurados como entradas. Las pull-ups se podrían equiparar con resistencias de entre 20 K y 50 K. a partir de dichos valores podemos calcular la corriente que puede fluir por ellas si están activadas. Las pull-ups se pueden habilitar pin por pin independientemente escribiendo un 1 en su registro de salida PORT. Las-pull ups solo serán efectivas en los pines que actúan como entradas; en los pines configurados como salidas las pull-ups quedan automáticamente deshabilitadas.
Existe un bit llamado PUD en el registro MCUCR cuya función es deshabilitar todas las pull-ups de todos los puertos si su valor es 1. El bit PUD (Pull-Ups Disable) inicializa a 0 y un posible interés por setearlo puede ser eliminar la pequeña corriente que puede fluir por las pull-ps cuando los pines en cuestión se conectan a 0 lógico. La siguiente figura muestra la conexión de un pulsador al AVR aprovechando la pull-up de un pin de E/S. Fíjate en que las pull-ups no se pueden usar como resistencias para excitar dispositivos como LEDs, relés, etc.
Ejemplo de uso de las resistencias de pull-up. La figura de ejemplo muestra la pull-up de un solo pin pero están presentes en todos los pines de E/S del AVR.
Configuración y Manejo de los Puertos Cuando los pines trabajan como entradas y salidas generales su control descansa principalmente en los Registros de E/S MCUCR, DDRx, PORTx y PINx, donde x puede ser A, B, C o D. Del registro MCUCR solo interviene el pin PUD, cuya función es deshabilitar las pull-ups de todos los pines cuando PUD = 1. Pero si PUD = 0, la habilitación de las pull-ups todavía requiere de cierta configuración por parte de los registros DDRx, PORTx y PINx. MCUCRJTDBODSBODSEPUD------IVSELIVCE Cada puerto tiene sus correspondientes registros DDRx, PORTx y PINx, así por ejemplo el puerto A tiene sus registros DDRA, PORTA y PINA. Lo mismo es aplicable a los otros puertos. PORTAPORTA7PORTA6PORTA5PORTA4PORTA3PORTA2PORTA1PORTA0 PINAPINA7PINA6PINA5PINA4PINA3PINA2PINA1PINA0 DDRADDA7DDA6DDA5DDA4DDA3DDA2DDA1DDA0
El registro PORTx es para escribir datos en los pines del puerto x que están configurados como salida. Ése es su uso habitual. Sin embargo, escribir en los bits de PORTx cuyos pines estén configurados como entradas significa activar o desactivar las pull-ups de dichos pines (ver la tabla de abajo). Ésta es la segunda función de PORTx. Leer el registro PORTx solo significa obtener el último valor que se escribió en este registro. El registro PINx es para leer el estado de los pines del puerto x, sin importar si los pines están configurados como entradas o como salidas. La función alternativa de este registro es para conmutar el estado de los pines configurados como salidas cuando se escribe un 1 en su bit correspondiente de PINx. El registro DDRx es para configurar la dirección del puerto x, es decir, para establecer cuáles pines serán entradas y cuáles serán salidas. (Data Direction Register = Registro de Dirección de Datos). Después de un reset todos los puertos inician con sus pines configurados como entradas, pero se pueden reconfigurar en cualquier punto del programa.
Si se escribe un 0 en un bit de DDRx, entonces el pin correspondiente en el puerto x será de entrada y si se escribe un 1, el pin será de salida. Detesto mencionar a los PICmicro, pero creo que te puede servir saber que la implicancia del 1 y el 0 en los PICmicros es al revés.
0 → entrada 1 → salida
Por ejemplo, si escribimos el valor 11110000 en DDRB, entonces los cuatro pines de menor peso del puerto B serán entradas digitales y los cuatro pines superiores serán de salida. Si escribimos 11111111 en DDRA, todos los pines del puerto A serán de salida, y si escribimos 00000000 en DDRB todo el puerto B será de entrada. La codificación de lo expuesto sería así: DDRA = 0xFF; // 0xFF = 0b11111111 DDRB = 0x00; // 0x00 = 0b00000000 Luego podremos leer y escribir en los puertos mediante PORTA y PINB, así. unsigned char regval; PORTA = 0x73; // Escribir 0b01110011 en el puerto A regval = PINB; // Leer puerto B
Hasta aquí estuvo todo muy fácil porque los puertos completos estaban configurados para entrada o salida. Ahora veremos casos de configuración mixta y lo que sucede, por ejemplo, si escribimos en PINx o si leemos de PORTx. Si además trabajamos con el pin PUD para habilitar las resistencias de pull-up, tendremos que valernos de una tabla para no enredarnos. Caso 1 2 3 4 5
Bit en DDRx 0 0 0 1 1
Bit en PORTx 0 1 1 0 1
Bit PUD (en MCUCR) X 0 1 X X
Dirección de Pin Entrada Entrada Entrada Salida Salida
Pull-up
Estado de Pin
No SÍ No No No
Tri-Estado (Alta impedancia) Alto, debido a la pull-up. Tri-Estado (Alta impedancia) Bajo (0 lógico) Alto (1 lógico)
Casos 4 y 5. Son los más simples. Si un pin está configurado como salida, de ningún modo tendrá pullup y su estado de salida será 1 ó 0 lógico, dependiendo del valor escrito en su respectivo bit de PORTx.
Caso 1. Si un pin está configurado como entrada y su bit respectivo en PORTx vale 0, el pin tampoco tendrá pull-up y su estado será de alta impedancia. Caso 2. Si un pin está configurado como entrada y su bit respectivo en PORTx vale 1, el pin tendrá pull-up y su estado se leerá como 1 lógico (por la pull-up), siempre que el bit PUD del registro MCUCR valga 0. Caso 3. Raramente se suele poner PUD a 1, pero si se hace, se deshabilitarán las pull-ups de todos los pines. Por tanto los pines de entrada serán siempre de alta impedancia.
Más adelante está una práctica de interface con teclado matricial donde se utiliza. La notación de números binarios empleando el prefijo 0b no es parte del C Estándar y aunque AVR GCC la admita, IAR C no la reconoce. Por tanto solo es recomendable el uso de la notación hexadecimal.
Control de Dispositivos Básicos Secuenciador de 3 Efectos Este programa está inspirado en el led flasher 2 de Seiichi Inoue y lo puedes ubicar en el sitio web http://www.piclist.com/images/www/hobby_elec/e_pic6_b.htm. Estos son los tres efectos que se pueden escoger.
Cada efecto se podrá seleccionar ingresando su correspondiente número mediante el teclado. El reto es que el primer LED alumbra a su plenitud, el segundo LED, un poquito menos y el tercero, como la colita de un cometa, brilla menos aún. ¿Cómo conseguiremos variar la intensidad de brillo de un LED? Regulando la cantidad de corriente que fluye por él. Y ¿cómo variamos su corriente sin emplear un potenciómetro o algo por el estilo? Bueno, las dos formas más habituales se conocen como modulaciones PWM y PRM, a la segunda de las cuales se apega la onda generada por el patrón de efecto de nuestro secuencial. PRM es la sigla de Modulación por Frecuencia de Pulsos, en inglés.
Ondas de corriente en los LEDs y su valor promedio.
Dado que los LEDs que parpadean dan miles de centelleos por segundo, nuestros ojos no lo podrán percibir así, solo veremos una disminución en su luminosidad. Es el valor promedio de la corriente lo que cuenta para el brillo del LED. No obstante, tampoco hay una proporción directa entre estos dos parámetros. Así un LED aparezca prendido la sexta parte del tiempo que otro, no significa que vaya a brillar 6 veces menos. Por otro lado, este hecho es aprovechable por otras aplicaciones como el control de varios displays de 7 segmentos o los letreros matriciales de LEDs, que veremos más adelante.
Circuito de la práctica. Para generar los efectos de desplazamiento de los LEDs y las ondas PRM de cada LED se emplea una matriz llamada Pattern. Es una gran matriz pero el software emplea índices relativos para dividirla en varios bloques. La separación de los bloques en tres grupos, uno para cada efecto, también es una división lógica. Cada uno de los bloques de la matriz Pattern es una posición en el desplazamiento de los LEDs. Por ejemplo, en la posición 2 del primer efecto se ejecutará el bloque 2 por 100 veces cíclicamente. El bloque 2 es el tercero porque en el mundo digital se empieza desde 0 y su patrón es el conjunto de datos
0xe7,0x24,0x24,0x66,0x24,0x24. No dice mucho porque está en hexadecimal, pero si lo pasamos a binario, que es como originalmente lo elaboré, y lo ordenamos de arriba abajo, tenemos esto 11100111 00100100 00100100 01100110 00100100 00100100 Como se ve, este bloque indica que en este lapso los bits 2 y 5 siempre estarán activos, los bits 1 y 6 se prenden en dos de seis partes, y los bits 0 y 7 solo se prenden la sexta parte del tiempo. Si no tenemos un osciloscopio de 8 canales, una gráfica de Proteus nos puede mostrar las ondas correspondientes. En la sección Gráficos de Simulación de la clase de Proteus se describe paso a paso cómo obtener este resultado. La única diferencia es que en esta ocasión la gráfica es de tipo DIGITAL en vez de ANALÓGICO.
/****************************************************************************** * FileName: main.c * Purpose: LED Flasher 3. Secuenciador de 3 efectos seleccionables vía USART * Processor: ATmega164P * Compiler: IAR-C y AVR-GCC (WinAVR) * Author: Shawn Johnson. http://www.cursomicros.com. * Basado en el led flasher 2 de Seiichi Inoue localizado en * http://hobby_elec.piclist.com/index.htm. *
* Copyright (C) 2008 - 2012 Shawn Johnson. All rights reserved. * * License: Se permiten el uso y la redistribución de este código con * modificaciones o sin ellas, siempre que se mantengan esta * licencia y las notas de autor y copyright de arriba. *****************************************************************************/ #include "avr_compiler.h" #include "usart.h" /****************************************************************************** Patrón que contiene el patrón de efecto del secuencial led1 111111111111111111111111111111... Prendido 6 de 6 partes led2 100100100100100100100100100100... Prendido 2 de 6 partes led3 100000100000100000100000100000... Prendido 1 de 6 partes - Cada bloque tiene 6 items - Cada bloque se repite 100 veces - Hay una pausa de 150 µs entre los ítems - Hay 12/11 bloques en total -> Cada barrido dura 6 * 100 * 150 * 12 = 1.08 segundos aprox. ******************************************************************************/ PROGMEM char Pattern[] = { // Efecto 1. 12 bloques de 6 items 0x81,0x81,0x81,0x81,0x81,0x81, 0xc3,0x42,0x42,0xc3,0x42,0x42, 0xe7,0x24,0x24,0x66,0x24,0x24, 0x7e,0x18,0x18,0x3c,0x18,0x18, 0x3c,0x18,0x18,0x18,0x18,0x18, 0x18,0x18,0x18,0x18,0x18,0x18, 0x3c,0x24,0x24,0x3c,0x24,0x24, 0x7e,0x42,0x42,0x66,0x42,0x42, 0xe7,0x81,0x81,0xc3,0x81,0x81, 0xc3,0x00,0x00,0x81,0x00,0x00, 0x81,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00, // Efecto 2. 11 bloques de 6 items 0x80,0x80,0x80,0x80,0x80,0x80, 0xc0,0x40,0x40,0xc0,0x40,0x40, 0xe0,0x20,0x20,0x60,0x20,0x20, 0x70,0x10,0x10,0x30,0x10,0x10, 0x38,0x08,0x08,0x18,0x08,0x08, 0x1c,0x04,0x04,0x0c,0x04,0x04, 0x0e,0x02,0x02,0x06,0x02,0x02, 0x07,0x01,0x01,0x03,0x01,0x01, 0x03,0x00,0x00,0x01,0x00,0x00, 0x01,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00, // Efecto 3. 11 bloques de 6 items 0x01,0x01,0x01,0x01,0x01,0x01, 0x03,0x02,0x02,0x03,0x02,0x02, 0x07,0x04,0x04,0x06,0x04,0x04, 0x0e,0x08,0x08,0x0c,0x08,0x08, 0x1c,0x10,0x10,0x18,0x10,0x10, 0x38,0x20,0x20,0x30,0x20,0x20, 0x70,0x40,0x40,0x30,0x40,0x40, 0xe0,0x80,0x80,0xc0,0x80,0x80, 0xc0,0x00,0x00,0x80,0x00,0x00, 0x80,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00 }; /***************************************************************************** * Main function ****************************************************************************/ int main(void) { unsigned int i, j, k, b, bi, ba; char c, Effect; DDRB = 0xFF;
// Setup PORTB for output
usart_init();
// Initialize USART @ 9600 N 1
puts("\n\r Escoja un efecto "); puts("\n\r (1) Barrido simétrico");
puts("\n\r (2) Barrido a la derecha"); puts("\n\r (3) Barrido a la izquierda"); Effect = '1';
// Por defecto iniciar con efecto 1
while(1) { start: /* Establecer parámetros de barrido del efecto actual */ switch(Effect) { case '1': ba = 0; b=12; break; case '2': ba = 6*12; b=11; break; case '3': ba = 6*(12+11); b=11; break; } /* Iniciar barrido del efecto actual */ for(i=0; i='1')) // { Effect = c; // goto start; // } } delay_us(150);
serie esperando recibir una efecto Si hay datos en el USART,.. Leer dato Si es una opción válida,... Tomar opción y reiniciar
} } } } }
Delays Antirrebote Cuando apretamos un pulsador o movemos un switch, la señal de tensión relacionada no cambiará su valor establemente, sino que se darán pequeños rebotes en el contacto que generarán ondas irregulares. A veces se puede implementar un sencillo filtro pasa-bajas para evadir estos rebotes. Como este circuito puede resultar algo incómodo de armar, casi siempre se prefiere añadir una rutina antirrebote en el programa.
Sencillo filtro antirrebote para un pulsador. De los tantos mecanismos realizables poner un delay es el más simple. Una vez detectado el cambio de tensión se espera un lapso de tiempo hasta que la señal se estabilice y luego se vuelve testear la entrada. Una variante es, luego de responder al primer pulso esperar un tiempo para que se estabilice la señal. En cuanto al enunciado del programa: cada vez que presionemos un botón (pulsador) se prenderá un LED más del puerto B y con otro botón se hará lo mismo para apagar un LED más. De modo que aprovecharemos el circuito de la práctica anterior. Para comprobar la utilidad de la aparente irrelevante rutina antirrebote, puedes probar lo que sucede si le quitas los Delays al programa.
Circuito de la práctica.
/****************************************************************************** * FileName: main.c * Purpose: Delays antirrebote * Processor: ATmega164P * Compiler: IAR-C y AVR-GCC (WinAVR) * Author: Shawn Johnson. http://www.cursomicros.com. * * Copyright (C) 2008 - 2012 Shawn Johnson. All rights reserved. * * License: Se permiten el uso y la redistribución de este código con * modificaciones o sin ellas, siempre que se mantengan esta * licencia y las notas de autor y copyright de arriba. *****************************************************************************/ #include "avr_compiler.h" int main(void) { char port = 0; DDRD = 0x00; PORTD = 0x0C;
// PORTD como entrada // Activar pull-ups de pines PD2 y PD3
DDRB = 0xFF; PORTB = 0x00;
// PORTB como salida // Limpiar PORTB
while(1) // Bucle infinito { if((PIND & 0x04)==0) // Si PD2 vale 0 (pulsador presionado) { port <<= 1; // Desplazar port a la izquierda port |= 0x01; // Setear bit 0 de port PORTB = port; // Colocar en PORTB delay_us(30000); // Delay antirrebote, 30ms while((PIND & 0x04)==0) // Mientras PD2 valga 0, esperar continue; } if((PIND & 0x08)==0) // Si { port >>= 1; port &= 0x7F; PORTB = port; delay_us(30000); while((PIND & 0x08)==0) continue; }
PD3 vale 0 (pulsador presionado) // // // // //
Desplazar port a la derecha Limpiar bit 7 de port Colocar en PORTB Delay antirrebote, 30ms Mientras PD3 valga 0, esperar
} }
Control de Displays 7 segmentos Un display 7 segmentos no es más que una matriz de 7 diodos LEDs dispuestos de forma que encendiéndolos apropiadamente se pueden formar los números del 0 al 9 y algunas letras del alfabeto. Se dividen en dos grupos: de ánodo común y de cátodo común. El que vamos a emplear en esta práctica pertenece al segundo grupo y se muestra abajo.
Display 7 segmentos de cátodo común. Como ves arriba, cada LED del display está identificado por una letra entre a y g. Algunos displays también cuentan con un led en la parte inferior llamado dp (decimal point), pensado para representar un punto decimal. Yendo a la práctica. Este programa controla 4 displays de 7 segmentos multiplexados. Esta técnica es muy conocida y consiste en prender los displays uno a uno en tiempos sucesivos pero con la suficiente frecuencia como para que parezcan que están prendidos los cuatro al mismo tiempo. Los datos que visualizarán los displays serán ingresados por el puerto serie de la PC.
Circuito de la práctica. En el programa se emplea el array bcdcodes para almacenar los códigos 7 segmentos de los números del 0 al 9. // Matriz de códigos hexadeximales. Formato: xgfedcba , x = don't care const char bcdcodes[] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};
Aquí tampoco se distingue mucho por estar en hexadecimal, pero convertido en binario y ordenado de arriba abajo lo notaremos mejor. Hexa ----0x3f 0x06 0x5b 0x4f
= = = =
binario ----------00111111 00000110 01011011 01001111
// // // //
7-segment 7-segment 7-segment 7-segment
code code code code
of of of of
0 1 2 3
0x66 0x6d 0x7d 0x07 0x07 0x7f
= = = = = =
01100110 01101101 01111101 00000111 01111111 01101111
// // // // // //
7-segment 7-segment 7-segment 7-segment 7-segment 7-segment
code code code code code code
of of of of of of
4 5 6 7 8 9
De acuerdo con el circuito, el display está conectado a los 7 primeros pines de PORTB del AVR (a con 0, b con 1... y g con 6). El pin 7 queda libre, por eso el bit 7 de cada dato no interesa, don‟t care. De acuerdo con el circuito, el display está conectado a los 7 primeros pines de PORTB del AVR (a con 0, b con 1... y g con 6). El pin 7 queda libre, por eso el bit 7 de cada dato no interesa, don‟t care. /****************************************************************************** * FileName: main.c * Overview: Control de 4 displays 7 segmentos con interface USART * Processor: ATmega164P * Compiler: IAR-C y AVR-GCC (WinAVR) * Author: Shawn Johnson. http://www.cursomicros.com. * * Copyright (C) 2008 - 2012 Shawn Johnson. All rights reserved. * * License: Se permiten el uso y la redistribución de este código con * modificaciones o sin ellas, siempre que se mantengan esta * licencia y las notas de autor y copyright de arriba. *****************************************************************************/ #include "avr_compiler.h" #include "usart.h" char GetNumStr(char * buffer, unsigned char len);
// Prototipo de función
int main(void) { // Matriz de códigos hexadeximales. Formato: xgfedcba , x = don't care const char bcdcodes[] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; unsigned int d, digit; signed char i; char buff[10] = "1234"; char buff2[10]; DDRB = 0xFF; DDRD = 0xF0;
// Iniciar con 1234 // Todo PORTB para salida // Nible alto de PORTD para salida
usart_init(); puts("\r\n Escribe un número para el display 7 segmentos \r\n"); while(1) { for(d=0; d<4; d++) // Para pasar por los 4 displays { if(GetNumStr(buff2, 4)) // Si se leyó una cadena de 4 númeroa strcpy(buff, buff2); // Actualizar cadena para visualizar i = strlen(buff)-d-1; if(i >= 0) digit = buff[i]; else digit = '0';
// Calcular posición de dígito // Obtener dígito // Rellenar con ceros a la izquierda
PORTD = 0xf0; PORTB = bcdcodes[digit-'0']; PORTD = PORTD & (~(0x10<
// Apagar displays temporalmente // Poner nuevo dato en display // Activar nuevo display
} } } //**************************************************************************** // Lee una cadena de texto de 'len' números // El tamaño de 'buffer' debe ser mayor que 'len'. //**************************************************************************** char GetNumStr(char * buffer, unsigned char len) { char c; static unsigned char i=0; if(kbhit()) { c = getchar(); if((c<='9'&&c>='0')&&(i0 { i--; // putchar(c); // Eco } else if((c=='\r')&&(i)) // Si c es ENTER y si i>0 { buffer[i] = '\0'; // Poner un 0x00 (fin de cadena) putchar(c); // Eco i = 0; // Resetear contador return 1; // Retornar con 1 } } return 0; // Retornar con 0 }
Retraso y Frecuencia de Repetición Antes, en los tiempos analógicos, gran parte de la interface de usuario de los aparatos electrónicos implicaba la presencia de algún tipo de potenciómetros y switches. Actualmente, una característica de los sistemas digitales es su manipulación basada en botones. Un botón para esto, otro para aquello... Inclusive a un mínimo de ellos se le puede dotar de múltiples funciones para el completo control de algún proceso. El programa de esta práctica implementa el concepto que tantas veces leemos como retraso de repetición frecuencia de repetición. De hecho, podemos encontrar este mecanismo en casi cualquier artefacto digital. Aprovechando que ya tenemos armado nuestro circuito de 4 displays 7 segmentos, haremos un contador que se incremente cada vez que se pulse un botón y se decremente cuando se pulse otro. Si un botón permanece apretado por más de cierto tiempo (retraso de repetición), se iniciará un incremento/decremento continuo, según la razón establecida (frecuencia de repetición).
Pero aquí no termina la cosa. Con criterio de buen diseñador deberíamos prever lo que pasaría si alguien presionara un botón sin antes haber soltado el anterior. Eso dependerá de la aplicación. En este programa yo decidí dejar sin efecto al segundo botón mientras el primero esté pulsado. Está de más decir que los rebotes deben ser filtrados.
Circuito de la práctica. La tarea de la función CheckButtons la puedes leer en su respectivo encabezado. Allí dice que si se llama periódicamente cada 100us el retraso de repetición será de 600ms y la frecuencia de repetición de 1/100ms = 10 incrementos/decrementos por segundo. Estos parámetros se pueden modificar editando los límites de conteo de las variables a y b. Tras ser detectada la señal negativa (botón pulsado), el programa seguirá testeando el pin continuamente durante 25 ms. Solo se reconocerá como válido el pulso que permaneció estable en este lapso. Eso servirá para filtrar los pequeños pulsos de rebote.
Una característica notable de este mecanismo, comparable solo con las interrupciones, es que permite atender a los botones sin necesidad de que el programa se trabe esperando a que se pulsen o liberen. Sin embargo, debo aclarar que el tiempo de los 100us no se cumple estrictamente y puede variar bastante de acuerdo con la aplicación. En esta práctica ni se nota, pero si realmente llegara a interesar se puede ajustar o superar con las interrupciones de algún timer. /****************************************************************************** * FileName: main.c * Purpose: Retraso de repetición y velocidad de repetición * Processor: ATmega164P * Compiler: IAR-C y AVR-GCC (WinAVR) * Author: Shawn Johnson. http://www.cursomicros.com. * * Copyright (C) 2008 - 2012 Shawn Johnson. All rights reserved. * * License: Se permiten el uso y la redistribución de este código con * modificaciones o sin ellas, siempre que se mantengan esta * licencia y las notas de autor y copyright de arriba. *****************************************************************************/ #include "avr_compiler.h" /* Prototipos de función */ void CheckButtons(void); void IncCNT(void); void DecCNT(void); /* Definiciones */ #define MaxCNT 5000 #define ButtonI ( PIND & (1<<2) ) #define ButtonD ( PIND & (1<<3) ) /* Variables globales */ volatile unsigned int CNT;
// Máximo Valor de CNT (arbitrario) // PD2 Botón de incremento // PD3 Botón de decremento
// Contador
/***************************************************************************** * Main function ****************************************************************************/ int main(void) { // Matriz de códigos hexadeximales. Formato: xgfedcba , x = don't care const char bcdcodes[] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; unsigned int i, j, digit; signed char idx; char buff[10]; DDRB = 0x7F; PORTB = 0x80; DDRD = 0xF0; PORTD = 0x0C;
// // // //
Los 7 bits altos de PORTB para salida 0 a pines de salida, sin Pull-up en pin de entrada Nible alto de PORTD para salida Activar Pull-ups de pines PD2 y PD3
CNT = 123; while(1) { for(j=0; j<100; j++) { for(i=0; i<4; i++)
// Inicializar contador
// Para pasar por los 4 displays
{ CheckButtons(); itoa(CNT, buff, 10); idx = strlen(buff)-i-1; if(idx >= 0) digit = buff[idx]; else digit = '0'; PORTD |= 0xf0; PORTB = bcdcodes[digit-'0']; PORTD &= (~(0x10<
// Convertir número en cadena // Calcular posición de carácter // Obtener carácter // // // //
Poner 0 en displays sin número Apagar displays temporalmente Poner nuevo dato en display Activar nuevo display
} } } } /***************************************************************************** * Mide el tiempo que los botones I y D están pulsados. Si dicho tiempo * llega a 25ms llamará a IncCNT (o DecCNT) y luego de 600ms la llamará * continuamente tras cada 100ms. Un botón no responderá mientras el * anterior no sea liberado. * Para cumplir los tiempos citados debería ser llamada cada 100us. ****************************************************************************/ void CheckButtons(void) { static unsigned int a; // Cuenta el tiempo que ButtonI está pulsado static unsigned int b; // Cuenta el tiempo que ButtonD está pulsado if((ButtonI==0)&&(b==0)) // Si ButtonI pulsado y ButtonD libre { a++; // Incrementar contador if(a==250) // Si llegó a 25 ms { IncCNT(); // Procesar por primera vez } else if (a>6250) // Si pasaron más de 600 ms { IncCNT(); // Procesar por segunda y demás veces a -= 1000; // Siguientes incrementos serán cada 100 ms } } else a=0; // Resetear contador if((ButtonD==0)&&(a==0)) // Si ButtonD pulsado y ButtonI libre { b++; // Incrementar contador if(b==250) // Si llegó a 25 ms { DecCNT(); // Procesar por primera vez } else if (b>6250) // Si pasaron más de 600 ms { DecCNT(); // Procesar por segunda y demás veces b -= 1000; // Siguientes incrementos serán cada 100 ms } } else b=0; // Resetear contador }
/***************************************************************************** * Incrementan o decrementan CNT. * El valor de CNT está limitado al rango [0 : MaxCNT] ****************************************************************************/ void IncCNT(void) { if(CNT0) CNT--; }
// Si Duty > 0 // Decrementar CNT
Control de Motor Paso a Paso A diferencia de un motor DC, que solo tiene una bobina y que empieza a girar apenas se le conecta la alimentación, con una velocidad que varía de acuerdo con el voltaje aplicado; los motores de pasos tienen cuatro bobinas y avanzan o retroceden solo un pequeño ángulo de giro, llamado ángulo de paso, por cada combinación de voltaje aplicada en sus boninas. Para mantener la marcha del motor es necesario cambiar periódicamente la combinación de voltajes en sus terminales. Con 4 bobinas un motor PAP (Paso A Paso) puede presentar hasta 8 terminales, 2 por cada bobina. Los terminales se pueden unir interna o externamente dando lugar a dos tipos de motores PAP: los unipolares y los bipolares. Puedes ver la siguiente figura para identificar el origen de tu motor PAP. Observa que si tienes un motor unipolar de 8 ó 6 terminales, lo puedes convertir en un motor bipolar; pero no es posible arreglar un motor bipolar para que trabaje como unipolar, a menos que separes sus bobinas internamente.
Motores de pasos Unipolares y Bipolares. Aunque tengan más terminales, los motores PAP Unipolares son más fáciles de manejar por el hardware requerido. En estos motores las bobinas se polarizan en una sola dirección, por lo que basta con un switch o transistor por cada bobina para energizarla. En cambio en los motores PAP bipolares cada bobina debe polarizarse en ambas direcciones. Esto es equivalente a hacer girar un motor DC en ambas direcciones; y tú sabes que para eso se requiere de un circuito llamado Puente-H. Para controlar un motor PAP Unipolar las bobinas deben ser polarizadas secuencialmente de acuerdo con la orientación que se le quiera dar al rotor. Para comprender esto debes recordar la ley de atracción y repulsión entre polos magnéticos, así como la ley de Lens, que explica la orientación del campo magnético generado por la corriente que fluye por una bobina. En el motor PAP unipolar no deben polarizarse las 4 bobinas al mismo tiempo porque generarían un campo magnético simétrico y el rotor no sabría a dónde girar. A partir de esto se deduce que existen hasta tres modos de hacer girar un motor PAP unipolar, pero los dos principales son:
Modo Full Step o de paso completo. Es cuando las bobinas se polarizan de dos en dos. En la siguiente animación los terminales medios están unidos interna o externamente al círculo que representa la alimentación positiva. Entonces para polarizar las bobinas se ponen a nivel bajo (azul) sus terminales
largos. He resaltado en amarillo cada bobina polarizada. Eso ayuda a distinguir la secuencia de polarización y a descubrir que no existen más que 4 combinaciones de polarización aplicables, las cuales deben repetirse cíclicamente. Los números hexadecimales al lado de la tabla se obtienen asumiendo que los puntos azules son ceros y los rojos unos. De hecho esa suposición no importa mucho, como tampoco importa que los terminales medios estén conectados a la alimentación positiva o a GND, ni que la secuencia de pasos inicie en 0x09. Puedes invertir las polarizaciones y al final te sorprenderá que el motor siga girando en la misma dirección. Lo único que hará que cambie de dirección es que reciba la secuencia de pasos en orden invertido.
Operación de un motor de pasos unipolar en modo Full step.
Modo Half Step o de paso medio. Es cuando las bobinas se polarizan de a una y de a dos intercaladamente. Si te fijas bien en la tabla de pasos, verás que también incluye los 4 pasos del modo full step. Obviamente esos son los momentos en que hay dos bobinas polarizadas, en los otros 4 pasos solo se polariza una bobina. La ventaja de este mecanismo respecto del modo Full step es que se pueden realizar movimientos de giro más finos. La desventaja es que puede disminuir el torque, o fuerza de giro del rotor.
Operación de un motor de pasos unipolar en modo Half step.
El programa de esta práctica soporta ambos modos de control, los cuales se establecen desde la consola mediante la tecla „*‟. Por cada vez que se pulse la tecla „+‟ el motor avanzará 1 ó medio paso, dependiendo del modo actual seleccionado y por cada vez que se pulse la tecla „-‟ el motor retrocederá 1 ó medio paso.
Circuito de la práctica. Puesto que la tabla de pasos del modo Half Step también incluye los pasos del modo Full Step, no fue necesario crear dos tablas separadas. Los 8 pasos del motor se encuentran en el array steps. /* Matriz de pasos */ const char steps[] = {0x09,0x0D,0x0C,0x0E,0x06,0x07,0x03,0x0B};
/****************************************************************************** * FileName: main.c * Overview: Control de motor PAP en modos Full step y Half step, vía USART * Processor: ATmega164P * Compiler: IAR-C y AVR-GCC (WinAVR) * Author: Shawn Johnson. http://www.cursomicros.com. * * Copyright (C) 2008 - 2012 Shawn Johnson. All rights reserved. *
* License: Se permiten el uso y la redistribución de este código con * modificaciones o sin ellas, siempre que se mantengan esta * licencia y las notas de autor y copyright de arriba. *****************************************************************************/ #include "avr_compiler.h" #include "usart.h" int main(void) { /* Matriz de pasos */ const char steps[] = {0x09,0x0D,0x0C,0x0E,0x06,0x07,0x03,0x0B}; signed char step; char modo, c;
// Marca el paso actual
DDRB = 0x0F; PORTB = 0x00;
// Configurar Nibble bajo para salida // Limpiar nibble bajo. Sin Pull-ups en nibble alto
step = 1;
// Iniciar en cualquier paso
modo = 0xff;
// Esto será Full step (0x00 para half step)
usart_init();
// Inicializar USART0 @ 9600 N 1
puts("\r\n puts("\r\n puts("\r\n puts("\r\n
Control de Motor PAP en Full step y Half step\r\n"); (*): Conmutar entre Full step y Half step"); (+): Avanzar 1 o 1/2 paso"); (-): Retroceder 1 o 1/2 paso");
while(1) // bucle infinito { if(kbhit()) // Esperar a que llegue un dato del puerto serie { c = getchar(); // Leer dato recibido switch(c) { case '*': modo = ~modo; // Conmutar modo if (modo) puts("\r\n Full step"); else puts("\r\n Half step"); break; case '+':
step++; if( modo && (step&0x01) )
// Medio paso adelante // En modo Full step y si es
step++; if(step>7) step = 0; PORTB = steps[step]; break;
//
step--; if( modo && (step&0x01) )
// Medio paso atrás // En modo Full step y si es
necesario...
case '-':
... medio paso más
// Dar la vuelta // Ejecutar el paso
necesario... step--; if(step<0) { step = 7;
//
... medio paso más
// Dar la vuelta
if(modo) step--; } PORTB = steps[step]; break;
// En full step empezar desde 6 // Ejecutar el paso
} } } }
Control de Teclado Matricial Un teclado matricial es un conjunto de botones (switches) dispuestos en forma de malla, de modo que no se requieran de muchas líneas para su interface. De hecho, la mayoría de los teclados (incluyendo quizá el de tu ordenador) funciona con una estructura similar. En esta práctica trabajaremos con un teclado de 4×4. Como se aprecia en la siguiente imagen, cada botón del teclado está conectado a alguna de las filas Row, por un lado; y por el otro, a alguna de las columnas Col.
Aspecto físico y estructura interna de un teclado. La siguiente figura esboza la conexión entre un microcontrolador y un teclado de 4×4. Obviamente, no se puede leer el estado de una tecla como un pulsador cualquiera. Pero es fácil darse cuenta de que una tecla pulsada establece la conexión entre una de las filas Row y una de las columnas Col.
Conexión de un teclado a un microcontrolador. Por ejemplo, al presionar la tecla „6‟ se unen las líneas Row 1 y Col 2. O sea, si sacamos un 1 (ó 0) por el pin de Row 1, también deberíamos leer un 1 (ó 0) en el pin de Col 2, o viceversa. Generalizando, solo hay un par Row-Col que identifica cada tecla. En consecuencia, para saber cuál fue la tecla pulsada debemos sondear una a una todas las combinaciones Row-Col. Una vez detectada la condición de circuito cerrado, se usa el par Row-Col para deducir la posición de la tecla pulsada. Luego de expuesta la relativa sencillez de este teclado podemos sentirnos ansiosos empezar a codificar el programa de control. Solo hay que poner especial cuidado en la dirección de los pines y su conexión. Un mínimo descuido causaría un cortocircuito que dañaría el AVR.
La funcionalidad del programa no puede ser más sencilla. El valor de cada tecla pulsada será enviado a la consola de puerto serie de la PC.
Circuito de la práctica. La función de bajo nivel para el teclado es keypad_scan. En el interior de esta función el nibble bajo de PORTB se configura como salida y el nibble alto, como entrada, con sus correspondientes pull-ups. Al salir, todo PORTB queda como entrada para facilitar su posible posterior uso para otras rutinas. Según mi código, el valor leído en las columnas cuando no hay teclas pulsadas debería ser „1‟ lógico, y „0‟ cuando si las hay. Para eso es necesario que dichas líneas estén sujetas a Vcc por medio de resistencias, de pull-ups. Si se deshabilitan las pull-ups, el programa no funcionará a menos que se las sustituyan por resistencias externas con la misma función. /****************************************************************************** * FileName: main.c * Purpose: Control de teclado matricial de 4x4 * Processor: ATmega164P * Compiler: IAR-C y AVR-GCC (WinAVR) * Author: Shawn Johnson. http://www.cursomicros.com. * * Copyright (C) 2008 - 2012 Shawn Johnson. All rights reserved.
* * License: Se permiten el uso y la redistribución de este código con * modificaciones o sin ellas, siempre que se mantengan esta * licencia y las notas de autor y copyright de arriba. *****************************************************************************/ #include "avr_compiler.h" #include "usart.h" #define #define #define
kpd_PORT kpd_PIN kpd_DDR
PORTB PINB DDRB
// Port write // Port read // Dirección de puerto
char keypad_read(void); void keypad_released(void); char keypad_scan(void); int main(void) { char tecla; usart_init();
// Inicializar USART0 @ 9600 N 1
puts("\r\n Control de Teclado Matricial de 4x4 \r\n"); while(1) { nop(); tecla = keypad_read(); if(tecla) { putchar(tecla); keypad_released(); } }
// Alguna otra tarea // Leer teclado // Sí hubo Tecla pulsada (si es diferente de 0) // Esperar teclado libre
} //**************************************************************************** // Escanea el teclado y retorna el valor ASCII de la tecla presionada por // al menos 25ms. En otro caso retorna 0x00. //**************************************************************************** char keypad_read(void) { char c1, c2; c1 = keypad_scan(); // Escanear teclado if(c1) // Si hubo alguna tecla pulsada { delay_us(25000); // Delay antirrebote c2 = keypad_scan(); // Escanear otra vez if( c1==c2 ) // Si Ambas teclas leídas son iguales return c2; // entonces aceptarla } return 0x00; } //**************************************************************************** // Espera hasta que el teclado quede libre. //**************************************************************************** void keypad_released(void) { delay_us(10); //
while(keypad_scan()) continue;
// Mientras se detecte alguna tecla pulsada // seguir escaneando.
} //**************************************************************************** // Escanea el teclado y retorna el valor ASCII de la primera tecla que // encuentre pulsada. De otro modo retorna 0x00. //**************************************************************************** char keypad_scan(void) { unsigned char Col, Row; char RowMask, ColMask; // Col0 Col1 Col2 Col3 const static char keys[] = {'7', '8', '9', 'A', // Row 0 '4', '5', '6', 'B', // Row 1 '1', '2', '3', 'C', // Row 2 '.', '0', '#', 'D'}; // Row 3 kpd_DDR = 0x0F; kpd_PORT = 0xF0; RowMask = 0xFE; for(Row=0; Row<4; Row++) { kpd_PORT = RowMask; delay_us(10); ColMask = 0x10;
// Nibble alto entrada, nibble bajo salida // Habilitar pull-ups del nibble alto // Inicializar RowMask a 11111110
// // Para que se estabilice la señal // Inicializar ColMask a 00010000
for(Col=0; Col<4; Col++) { if((kpd_PIN&ColMask)==0) // Si hubo tecla pulsada { kpd_DDR = 0x00; // Todo puerto entrada otra vez return keys[4*Row+Col]; // Retornar tecla pulsada } }
ColMask <<= 1;
// Desplazar ColMask para escanear // siguiente columna
RowMask <<= 1; RowMask |= 0x01;
// Desplazar RowMask para escanear // siguiente fila
} // Se llega aquí si no se halló ninguna tecla pulsada kpd_DDR = 0x00; // Todo puerto entrada otra vez return 0x00; // Retornar Código de no tecla pulsada }
Letrero Matricial de LEDs Un panel matricial es uno de los proyectos más atractivos en el mundo de la electrónica. Su elaboración puede ser sencilla por su funcionamiento, aunque algo complicada por la implementación del hardware. En esta oportunidad aprenderemos a diseñar un panel de 8 filas y de 64 columnas, es decir, de 512 LEDs, pero verás que ampliar o reducir el tamaño será tan simple como añadir o quitar registros en el hardware o cambiar un solo número en el software.
El hardware
Sabemos que para encender un LED necesitamos de una señal de control, aparte de la alimentación (Vcc o GND), ¿cierto? Con esto en mente deberíamos suponer que para un letrero de 515 LEDs se necesitarían de 512 señales saliendo del microcontrolador, más o menos como en la siguiente figura: Pero no es así. Podemos resolver parte de este problema multiplicando las señales del microcontrolador con ayuda de dispositivos como multiplexores, decodificadores o registros serie-paralelo como el 74164, 74595 o el CD4094. Los dos últimos son muy parecidos y son a la vez mejores que los primeros porque cuentan con doble buffer. Uno para almacenar internamente los datos seriales que van ingresando al registro y otro que se conecta al exterior. Más adelante veremos los beneficios de esta arquitectura. Todos estos registros son de 8 bits pero tienen la característica de poder ser montados en cascada para multiplicar sus salidas. Por ejemplo, en la siguiente figura se muestra cómo conectar varios registros 74595 en cascada. Se pueden ir añadiendo tantos registros como salidas paralelas se desee. Por otro lado, si nos basamos solo en este mecanismo para ampliar nuestras señales, para controlar los 512 LEDs tendríamos que usar 512/8 = 64 registros de 8 bits, lo cual nos llevaría a un circuito muy difícil de implementar además de bastante costoso. La técnica para salvar este segundo inconveniente es un artificio que consiste en encender grupos de LEDs en tiempos diferentes pero con la suficiente frecuencia como para dar la impresión de que estuvieran encendidos todos al mismo tiempo. Obviamente, en un letrero matricial los LEDs quedan mejor agrupados en filas y/o columnas. En la siguiente figura los ánodos de los LEDs se unen formando las columnas y los cátodos se unen formando las filas (rows). También se puede armar una configuración alternativa invirtiendo la polaridad de todos los LEDs. En ese caso los transistores serán de tipo PNP. Los valores de las resistencias R1 a R64 dependen de la corriente que tiene que fluir por los LEDs, la cual a su vez depende de los mismos LEDs. Hay que tener en cuenta que los LEDs no estarán prendidos al 100 % sino la octava parte (por las ocho filas) y también que la corriente promedio no siempre es proporcional al brillo del LED prendido, es decir, que un LED esté prendido la octava parte no significa que vaya a brillar ocho veces menos. Por otro lado, los transistores deben tener la capacidad de controlar la corriente proveniente de todos los LEDs de cada fila. En algunos casos bastará con usar el ULN2803. Los barridos
Una vez estructurado el hardware de la matriz de LEDs nos damos cuenta de que podemos encender los LEDs que queramos de cualquier fila o de cualquier columna simplemente activando las coordenadas de dichos LEDs. Por ejemplo, si queremos prender los LEDs de las columnas 0, 3, 4, 7 y 63 de la fila 5 se vería así: Sin embargo, no es posible encender varios LEDs que pertenezcan a diferentes filas y diferentes columnas al mismo tiempo. Es aquí donde entra a jugar el software. Por ejemplo en la siguiente animación se muestra como para visualizar la letra G se encienden los LEDs correspondientes pero en tiempos diferentes. La primera figura muestra la secuencia del barrido en cámara lenta pero en la práctica los barridos serán tan rápidos que los LEDs se verán como en la segunda figura.
Los caracteres
De lo visto anteriormente queda claro que encendiendo los LEDs convenientemente podemos formar en el letrero la figura que deseemos. Será el microcontrolador quien de acuerdo con su programa se encargue de generar los barridos activando las filas y columnas adecuadamente según un patrón establecido. Este patrón corresponde a letras, figuras o números que queramos y se puede estructurar de diversas formas. Vamos a representar el patrón con una matriz de datos, donde cada dato represente una columna del panel de LEDs. De esta forma, si asignamos un 0 a un LED apagado y un 1 a un LED prendido, podemos establecer a partir de cada columna un byte de dato. Luego podremos agrupar ordenadamente todos estos datos y escribirlos en un formato conocido. Por ejemplo, para el pequeño texto de arriba la matriz escrita en lenguaje C quedaría algo así: const char matrix[] = {0x00, 0xFF, 0x10, 0x28, 0x44, 0x82, 0x00, 0xFF, 0x11, 0x31, 0x51, 0x8E, 0x00, 0x00, 0x00}; Esta matriz puede tener cualquier nombre pero de aquí en adelante me referiré a ella como matrix. Generación automática de matrix
Ya vimos que para generar nuestra matrix que se visualizará en el panel de LEDs hace falta conocer el sistema binario y/o hexadecimal. Pero para quienes no tengan la paciencia de componerla manualmente sobre todo si quieren experimentar con textos grandes, les presento una de varias herramientas que encontré en Internet. Se llama LCD font maker y, aunque fue diseñado para componer patrones de caracteres o gráficos para displays LCD o GLCD, también nos servirá para nuestro panel de LEDs. Su uso es bastante fácil de descubrir, así que no entraré en muchos detalles. Los pasos que debemos seguir son:
Presiona el botón Choose font y escoge la fuente que desees. Yo escogí Verdana-Negrita-11 porque he visto que produce un tipo de letra que se ajusta bien a la altura del letrero. Puedes probar por tu cuenta para ver otros resultados. En Char input ingresa el texto que mostrará tu letrero; Yo dejé unos espacios al principio para que el texto empiece a aparecer desde el costado derecho. Con "Offset" puedes centrar y ajustar vertical y horizontalmente el patrón del texto. Hay que establecer "Height" (altura) a 8 y "Width" (ancho) lo ajustamos hasta que cubra todo el texto, en mi caso fue de 230. Ahora presiona el botón "Step 2: open the fonts dialog parameters" y en la ventana que se abre escoge los parámetros que indica la siguiente figura. Presiona el botón "Step 3: Making a single fonts with the current graphics" para generar la matriz hexadecimal. El resultado aparecerá como se muestra en la siguiente figura. Puedes seleccionarlo y copiarlo manualmente o mediante el botón "Copy all". Guarda la matriz generada para usarla en el código fuente. Esta matriz está por defecto declarada como "unsigned char code Bmp001" pero lo cambiaremos luego.
El código fuente
La elaboración del código dependerá de varios factores, como el tamaño del panel, la forma cómo se presenta el texto (efecto), la longitud de los mensajes, de si los mensajes son estáticos o si se programan en tiempo de ejecución, etc. En esta práctica el panel solo muestra un mensaje en desplazamiento. Por tanto el código fuente será muy simple.
/****************************************************************************** * FileName: main.c * Overview: LED sign. Letrero Matricial de LEDs. * Processor: ATmega164P * Compiler: IAR-C y AVR-GCC (WinAVR) * Author: Shawn Johnson. http://www.cursomicros.com. * * Copyright (C) 2008 - 2012 Shawn Johnson. All rights reserved. * * License: Se permiten el uso y la redistribución de este código con * modificaciones o sin ellas, siempre que se mantengan esta * licencia y la nota de autor de arriba. *****************************************************************************/ #include "avr_compiler.h" #define SDATA_HIGH() #define SDATA_LOW()
PORTD |= (1<<5) PORTD &= ~(1<<5)
#define SCLOCK_HIGH() PORTD |= (1<<6) #define SCLOCK_LOW() PORTD &= ~(1<<6) #define SOEN_HIGH() #define SOEN_LOW()
PORTD |= (1<<7) PORTD &= ~(1<<7)
#define WIDTH 64 PROGMEM char matrix[] = { /*---------------------------------------------------------------------------Source file / text : www.cursomicros.com Width x Height (pixels) :230X8 ----------------------------------------------------------------------------*/ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x1F,0xFC,0xE0, 0x7C,0x0F,0x0F,0x7C,0xE0,0xFC,0x1F,0x03,0x00,0x03,0x1F,0xFC,0xE0,0x7C,0x0F,0x0F, 0x7C,0xE0,0xFC,0x1F,0x03,0x00,0x03,0x1F,0xFC,0xE0,0x7C,0x0F,0x0F,0x7C,0xE0,0xFC, 0x1F,0x03,0x00,0x00,0xE0,0xE0,0x00,0x00,0x3C,0x7E,0xC3,0x81,0x81,0x81,0x42,0x00, 0x7F,0xFF,0x80,0x80,0x80,0x40,0xFF,0xFF,0x00,0xFF,0xFF,0x02,0x03,0x03,0x03,0x00, 0x4E,0x9F,0x99,0x99,0x99,0xF9,0x72,0x00,0x3C,0x7E,0xC3,0x81,0x81,0xC3,0x7E,0x3C, 0x00,0xFF,0xFF,0x02,0x01,0x01,0xFF,0xFE,0x02,0x01,0x01,0xFF,0xFE,0x00,0x00,0xFF, 0xFF,0x00,0x00,0x3C,0x7E,0xC3,0x81,0x81,0x81,0x42,0x00,0xFF,0xFF,0x02,0x03,0x03, 0x03,0x00,0x3C,0x7E,0xC3,0x81,0x81,0xC3,0x7E,0x3C,0x00,0x4E,0x9F,0x99,0x99,0x99, 0xF9,0x72,0x00,0x00,0xE0,0xE0,0x00,0x00,0x3C,0x7E,0xC3,0x81,0x81,0x81,0x42,0x00, 0x3C,0x7E,0xC3,0x81,0x81,0xC3,0x7E,0x3C,0x00,0xFF,0xFF,0x02,0x01,0x01,0xFF,0xFE, 0x02,0x01,0x01,0xFF,0xFE,0x00 }; const unsigned int LEN = sizeof(matrix); /***************************************************************************** * Main function ****************************************************************************/ int main(void) { unsigned int bad; // Base index unsigned int idx; // Index unsigned char dato; // dato
unsigned char row; unsigned char col; unsigned char i;
// Fila // Columna
DDRD = 0xE0; DDRB = 0xFF; PORTD = 0x00; PORTB = 0x00; while(1) { for(bad=0; bad
// Si el bit de row es 1 // colocar 1 en DS
SCLOCK_HIGH(); nop();nop(); SCLOCK_LOW();
// Pulso de reloj para // validar el dato colocado
} PORTB = 0x00
// o, colocar 0
// Desactiva todas las filas
temporalmente
} } } } }
descargar
SOEN_HIGH(); nop();nop(); SOEN_LOW();
// Pulso para sacar todos // los datos cargados
PORTB = row;
// Activar la fila actual
La simulación
Dudo que todos los lectores consigan implementar en la práctica real un letrero matricial completo debido a la relativa complejidad del hardware, pero creo que al menos podrán ver su diseño en una buena simulación gracias a Proteus. Debemos notar que para la simulación en Proteus no es necesario armar el circuito completamente. Para este diseño por ejemplo he ignorado las resistencias y los transistores de las columnas y filas del panel. Se puede (o debe) editar el parámetro Minimum Trigger Time de las matrices de LEDs para mejorar la visualización de los LEDs sobre todo si se cambia la frecuencia del XTAL. El realismo de simulación también dependerá de la potencia de ordenador. En ordenadores lentos el contenido del panel se desplaza más lentamente, aunque se puede mejorar la animación modificando algunos parámetros, como la cantidad de barridos por frame en el código fuente, solo para fines de la simulación.
Los Displays LCD
Introducción Este capítulo está dedicado a los LCDs alfanuméricos con controlador Hitachi HD44780 o compatible, es decir, la mayoría. Hay diversas firmas, como Optrex, Sharp, Crystalfontz America, Tianma, etc., que producen muchísimos LCDs de este tipo. Los hay desde 1 a 4 líneas, desde 8 a 40 letras por línea, algunos con iluminación de fondo, con diferente tecnología de fabricación, etc. Dada la compatibilidad en el control de todos ellos, la elección de un modelo en particular queda a tu cargo. El LCD utilizado en este curso es de 2 líneas, de 16 letras cada una.
Un display LCD de 2 líneas, de 16 caracteres cada una. Si bien es necesario conocer un dispositivo para sacarle el máximo provecho, en primera instancia a la mayoría de los aficionados solo le interesa ponerlo en práctica aunque sea de forma limitada. Si eres uno de ellos, y por el momento quieres ahorrarte algo de tiempo, puedes saltar a la sección Interface de un Display LCD.
Pines del LCD
Número de Pin 1 2 3 4 5 6 7...14
Símbolo Vss Vcc o Vdd Vee o Vo RS R/W E DB0...DB7
Pines del LCD. Algunos LCDs con iluminación disponen de dos pines adicionales para encenderla. Aun así, los 14 pines aquí citados siempre deberían coincidir. Nombre de señal DB0-DB7 E
Función 8 líneas de bus de datos. Para transferencia bidireccional de datos entre el µC y el módulo LCD. DB7 también se puede usar como bit busy flag. En operación de 4 bits solo se usa el nibble alto. Enable – Señal de inicio de operación de lectura/escritura. Señal para seleccionar operación de lectura o escritura.
R/W
“0” : Escribir en LCD “1” : Leer de LCD Register Select
RS
“0” : Registro de comandos (escritura). : Busy flag + puntero de RAM (lectura). “1” : Registro de datos (escritura, lectura). Acceso a DDRAM o CGRAM. Ajuste de contraste del LCD. Vee = GND es máximo contraste. Alimentación = +5 V típicamente. Alimentación = 0 V (GND).
Vee o Vo Vdd o Vcc Vss
Memorias del LCD CGROM - Character Generator ROM Es la zona de memoria donde se encuentran grabados los patrones de todos los caracteres que puede visualizar el LCD de fábrica. Tiene grabados cerca de 200 (varía mucho) tipos de caracteres de 5×7 puntos (lo más común) o 32 caracteres de 5×10 puntos. Este último modo es raramente usado porque no todos los modelos lo soportan.
Tabla estándar de caracteres de la CGROM.
DDRAM - Display Data RAM La DDRAM almacena los códigos de las letras que se visualizan en la pantalla del LCD. Tiene capacidad de 80 bytes, un byte por carácter si la fuente es de 5×7 puntos. Observa que no siempre se podrán visualizar los 80 caracteres. Por ejemplo, si quisiéramos mostrar el mensaje Hello en la pantalla, deberíamos enviar a la DDRAM los códigos ascii de cada letra de esa palabra. El controlador interno del LCD tomará esos códigos para buscar en la CGROM sus correspondientes patrones de visualización y luego los mostrará en la pantalla. La siguiente figura muestra la correspondencia entre las locaciones de la DDRAM y las posiciones de las letras que vemos en la pantalla de un LCD de 2 líneas, particularmente de uno de 2×16. Fíjate en que los 80 bytes de la DDRAM se dividen en dos sectores de 40 bytes, un sector por línea, así:
Línea 1, con sector de DDRAM desde 0x00 hasta 0x27. Línea 2, con sector de DDRAM desde 0x40 hasta 0x67.
Por lo tanto, podemos entender que siempre tenemos un LCD virtual de 2×40; aunque solo podamos ver 8, 16 ó 20 letras por cada línea. Los otros datos escritos en la DDRAM permanecen allí aunque no se visualicen.
Posiciones en DDRAM de las letras de la pantalla (números en hexadecimal).
CGRAM - Character Generator RAM La CGRAM es una RAM de 64 bytes donde el usuario puede programar los patrones de nuevos caracteres gráficos, ya sean de 5×7 puntos (hasta 8 caracteres) o de 5×10 puntos (hasta 4 caracteres). Este tema lo detallaré en la práctica final.
El Puntero de RAM Llamado también Address Counter, es un registro que sirve para acceder a las memorias RAM del LCD. Por ejemplo, si el Puntero de RAM vale 0x00, accedemos a la locación de DDRAM (o CGRAM) de esa dirección. Ahora bien, solo hay un puntero de RAM que trabaja con las dos RAMs del LCD, y para saber a cuál de ellas accede actualmente debemos ver la instrucción enviada más recientemente. Las instrucciones Clear Display, Return Home y Set DDRAM Address designan el Puntero de RAM a la DDRAM, mientras que Set CGRAM Address lo designa a la CGRAM. Afortunadamente, en la gran mayoría de los casos, el Puntero de RAM estará apuntando a la DDRAM. Además, en este caso viene a representar la posición del cursor (visible o no) del LCD en la pantalla.
Set Instrucciones del Display LCD Es el controlador interno HD44780 (u otro) del LCD quien ejecutará las operaciones de mostrar las letras en la pantalla, mover el cursor, desplazar el contenido de la pantalla, etc. Lo que nos toca a nosotros es enviarle los códigos de esas operaciones. A continuación, un resumen. Instrucciones
Código
Instrucciones de comando
RS R/W DB7 DB6 DB5
DB4
DB3
DB2
DB1
DB0
Clear Display Return Home
0 0 0 0
0 0
0 0
0 0
0 0
0 0
0 0
0 1
1 ×
Entry Mode Set
0 0
0
0
0
0
0
1
I/D
S
0 0
0
0
0
0
1
C
B
0 0
0
0
0
1
×
×
0 0
0
0
1
×
×
0 0 0 0
0 1
1 Puntero de RAM (CGRAM) Puntero de RAM (DDRAM)
Display ON/OFF Control Cursor or Display Shift Function Set
Set CGRAM Address Set DDRAM Address Read Busy Flag & RAM Pointer Write to CGRAM Instrucciones or DDRAM de datos Read from CGRAM or DDRAM
0 1
BF
DL
D
S/C
R/L
N
F
Puntero de RAM (DDRAM o CGRAM)
1 0
Escribir dato
1 1
Leer dato
Conviene saber que las instrucciones Clear Display y Return Home tienen un tiempo de ejecución de cerca de 1.52 ms. Las demás toman algo de 40 µs. El LCD cuenta con dos registros internos principales, que dividen, grosso modo, las instrucciones en de datos y de comando. Poniendo el pin RS = 1 accedemos al registro de datos y mediante él a cualquier locación de la DDRAM o CGRAM, para operaciones de lectura y escritura de datos. Con RS = 0 accedemos al registro de comandos para escribir instrucciones de control del LCD (Clear Display, Function Set, etc.). En el caso de una lectura, obtenemos un dato particular que contiene el valor del puntero de RAM junto con el bit Busy flag. Clear display: 0 0 0 0 0 0 0 1
Limpia toda la pantalla del LCD. También retorna el cursor a su posición inicial (cima izquierda), esto es, designa el puntero de RAM a la dirección 0x00 de la DDRAM. Return home: 0 0 0 0 0 0 1 x
Regresa el cursor a su posición inicial pero sin alterar el texto del display, es decir, solo designa el puntero de RAM a la dirección 0x00 de la DDRAM. Entry mode set: 0 0 0 0 0 1 I/D S
Establece el modo de incremento o decremento y modo de desplazamiento del LCD.
I/D = 1: El puntero de RAM se incrementa en 1 después de leer o escribir un dato. Así accedemos automáticamente a la siguiente locación de DDRAM o CGRAM. Si es DDRAM, este puntero representa la posición del cursor en la pantalla y el incremento significa su avance a la derecha. I/D = 0: El puntero de RAM se decrementa en 1 después de leer o escribir un dato. S = 1: Si se escribe un nuevo dato de carácter en el LCD, entonces el display entero se desplaza a la derecha cuando I/D = 0 o a la izquierda cuando I/D = 1. S = 0: El display no se desplaza luego de escribir en la DDRAM. Esto es lo usual.
Display on/off control: 0 0 0 0 1 D C B
Prende o apaga el Display, el Cursor y la función Blink del cursor.
D = 1: El display se prende. D = 0: Apaga el display. (No significa que los datos de las RAMs se vayan a borrar.) C = 1: Despliega el cursor. C = 0: No despliega el cursor B = 1: La letra indicada por el cursor parpadea. B = 0: La letra no parpadea.
Cursor or display shift: 0 0 0 1 S/C R/L x x
Desplaza el cursor o el display a la derecha o la izquierda sin escribir o leer datos. S/CR/LOperación 0 0 Mueve el cursor a la izquierda (puntero de RAM se decrementa en 1) 0 1 Mueve el cursor a la derecha (puntero de RAM se incrementa en 1) 1 0 El Cursor y el display entero se desplazan a la izquierda 1 1 El Cursor y el display entero se desplazan a la derecha Function set: 0 0 1 DL N F x x
Configura la longitud del bus de datos, el número de líneas y el tipo de fuente.
DL = 1 : La interface con el LCD es mediante un bus de datos de 8 bits. DL = 0 : La interface con el LCD es mediante un bus de datos de 4 bits. N = 1: Configura un display de 2 líneas. N = 0: Configura un display de 1 línea. F = 0: Fuente de carácter de 5×7 puntos. F = 1: Fuente de carácter de 5×10 puntos.
Set DDRAM address: 1AAAAAAA
Designa el puntero de RAM a lanueva dirección AAAAAAA de la DDRAM. Digamos que sirve para controlar la posición del cursor del LCD. Ejemplo, para escribir un texto en la segunda línea del display (que tiene dirección inicial 0x40), primero habría que enviar el comando Set DDRAM Address con el número 0x40 en el parámetro AAAAAAA.
Set CGRAM address: 01AAAAAA
Designa el puntero de RAM a la nueva dirección AAAAAAA de la CGRAM. Read Busy Flag & RAM Pointer: BF AAAAAAA
Leer bit Busy Flag (BF) y el valor del puntero de RAM. BF = 1 indica que una operación interna está en progreso. El LCD no aceptará una nueva instrucción hasta que BF sea 0. El valor de AAAAAAA leído representa el valor del puntero de RAM. Es posible prescindir del bit BF. Para ello debemos esperar el tiempo adecuado antes de enviar la siguiente instrucción. Write data to CGRAM / DDRAM: DDDDDDDD
Escribe el dato de 8 bits DDDDDDDD en la DDRAM o CGRAM, dependiendo de cuál de las dos esté siendo direccionada actualmente. Después de la escritura el puntero de RAM se incrementa o decrementa, según se haya configurado el display. Ver instrucción Entry Mode Set. Read data from CGRAM / DDRAM: DDDDDDDD
Lee un dato de 8 bits de la DDRAM o CGRAM, dependiendo de cuál de ellas esté siendo direccionada actualmente. Después de la lectura el puntero de RAM se incrementa o decrementa en uno, según la configuración del display. Ver instrucción Entry Mode Set.
Inicialización del LCD Los LCDs tienen un circuito interno de reset que lo inicializa automáticamente tras alimentar el LCD. Lo cierto es que la auto-inicialización no siempre es fiable. Por eso existe la inicialización por software, que permite una completa configuración de los parámetros del LCD. Su defecto es que es bastante exótico (Un poco más y nos piden que bailemos tap:). Se constituye de una serie de pasos que, por si fueran poco, varían de acuerdo con la interface de 4 u 8 bits a usar y con el empleo o no del bit Busy Flag. Los siguientes flowcharts corresponden a las inicializaciones del LCD para operar con interface de 4 y 8 bits y usando el bit busy flag.
Inicialización por software del LCD con interface de 4 bits.
Inicialización por software del LCD con interface de 4 bits.
Interface de un Display LCD
Aunque los LCDs parezcan simples de usar, para bien o para mal sus características abren puertas a diversos modos de interface. Aquí, algunos puntos de consideración.
Bus de datos. Estos LCDs ofrecen la posibilidad de ser controlados utilizando los 8 bits de su bus de datos o solo 4. Un modo de operación del LCD (con ventajas y desventajas) le permite trabajar sin conectar el pin R/W al microcontrolador. En ese modo pin R/W siempre debe plantarse a tierra y el programa deberá esperar los tiempos adecuados para que se ejecuten las instrucciones. Los LCDs están fabricados con tecnología CMOS, lo que deriva en la sugerencia de conectar los pines de entrada no usados a alguna señal estable para evitar que por ellos se filtre algún ruido que pueda perturbar la operación del LCD. LCDs con iluminación de fondo. Esta característica se basa en diferentes tecnologías, siendo la más habitual el empleo de una matriz de LEDs colocados detrás de la pantalla. Tú sabes que hay todo tipo de LEDs: algunos prenden a penas, mientras que otros, con la misma corriente, pueden servir de faros (bueno, casi :). Creo que eso da cuenta de lo mucho que puede variar el uso de la iluminación de un modelo a otro. La iluminación suele activarse con los pines 15 y 16, pero su polaridad también varía entre modelos. Sobra decir, por tanto, que sería mejor que chequees el datasheet de tu LCD si es uno de estos. Como sea, los pines que activan la iluminación suelen ser independientes de los 14 estándares y las prácticas de este curso deberían funcionar con iluminación o sin ella.
Librería Para Display LCD Tenemos a continuación una librería para controlar un LCD con una interface de 4 bits y usando el bit BF (Busy Flag). Si tuviste la paciencia de leer las páginas anteriores, verás que es un claro reflejo de todo lo expuesto. Y si no, de todos modos en seguida citaré ligeramente cómo utilizarla. La librería trabaja para los compiladores IAR C y AVR GCC y consta de dos archivos lcd.h y lcd.c. Ambos deberán ser indexados al proyecto en el entorno de IAR C o Studio 5 para WinAVR (ante alguna duda puedes ir a la sección Adición de Archivos o Librerías al Proyecto). En el código fuente, sin embargo, solo se debe indicar el archivo de cabecera i2c.h mediante la directiva #include. #include "lcd.h"
La librería utiliza por defecto el puerto B del AVR tanto para el bus de datos del LCD como para las líneas de control E, RS y R/W. Esta interface se puede modificar en los #defines del archivo i2c.h. Nota que por cada puerto se deben cambiar los tres registros, PORT, DDR y PIN. Esa podría ser una configuración de cierta recurrencia, en cambio, no deberíamos tocar lcd.c, salvo que por alguna razón deseemos editar su código.
Funciones Básicas Disponibles
lcd_init(). Obviamente es la primera función a llamar. Tras ejecutarse el LCD debe quedar inicializado, con la pantalla limpia y con el cursor en el primer casillero. lcd_gotorc(r,c). El LCD tiene un cursor que, si bien puede mostrarse en pantalla, suele configurarse para que permanezca oculto. Bien, visible o no, el cursor avanza automáticamente tras cada letra que se escribe. Con lcd_gotorc(r,c) podemos mover el cursor manualmente a la fila r y columna c. El parámetro r puede valer entre 1 y 2, y el valor de c va de 1 en adelante.
lcd_puts(s). Visualiza la cadena s en el LCD empezando en la posición actual del cursor. La cadena s es almacenada en RAM. No se suelen mostrar grandes datos en un LCD, así que no implemente una función análoga que opere sobre la memoria FLASH. lcd_clear(). Limpia la pantalla del LCD y coloca el cursor al inicio, en la fila 1, columna 1. lcd_data(c). Escribe una sola letra en el LCD, en la posición actual del cursor. lcd_data() también permite crear caracteres gráficos, como se muestra en una práctica más adelante. lcd_cmd(com). Ocasionalmente también usaremos esta función para enviar comandos al LCD, por ejemplo: lcd_cmd(LCD_LINE2); // Mover cursor al inicio de línea 2 lcd_cmd(LCD_CLEAR); // Limpiar pantalla lcd_cmd(LCD_CURBLK); // Mostrar Cursor + Blink lcd_cmd(LCD_CURSOR); // Mostrar solo Cursor lcd_cmd(LCD_CGRAM+7); // Mover Puntero de RAM a dirección 7 de la CGRAM
Las constantes LCD_CLEAR y algunas más se hallan definidas en el archivo lcd.h. Por supuesto que también se pueden formar cualesquiera códigos de instrucciones de los estudiados antes. /****************************************************************************** * FileName: lcd.h * Purpose: Librería de funciones para controlar un display LCD con chip * Hitachi HD44780 o compatible. La interface es de 4 bits. * Processor: ATmel AVR * Compiler: IAR-C y AVR-GCC (WinAVR) * Author: Shawn Johnson. http://www.cursomicros.com. * * Copyright (C) 2008 - 2012 Shawn Johnson. All rights reserved. * * License: Se permiten el uso y la redistribución de este código con * modificaciones o sin ellas, siempre que se mantengan esta * licencia y las notas de autor y copyright de arriba. *****************************************************************************/ #include "avr_compiler.h" //**************************************************************************** // CONFIGURACIÓN DE LOS PINES DE INTERFACE //**************************************************************************** /* Define el puerto a donde se conectará el bus de datos del LCD * Se utilizará el nible alto del puerto escogido (ejem. PB4-DB4,...,PB7-DB7) */ #define lcd_DATAout PORTB // Registro PORT del puerto #define lcd_DATAin PINB // Registro PIN del puerto #define lcd_DATAddr DDRB // Registro DDR del puerto /* Define el puerto a donde se conectarán las líneas de control del LCD * E, RW y RS. Puede ser el mismo puerto del bus de datos. */ #define lcd_CTRLout PORTB // Registro PORT del puerto #define lcd_CTRLin PINB // Registro PIN del puerto #define lcd_CTRLddr DDRB // Registro DDR del puerto /* Define los números de los pines del puerto anterior que corresponderán a * las líneas E, RW y RS del LCD. */ #define lcd_E 3 // Pin Enable #define lcd_RW 2 // Pin Read/Write #define lcd_RS 1 // Pin Register Select
//**************************************************************************** // CÓDIGOS DE COMANDO USUALES //**************************************************************************** #define LCD_CLEAR 0x01 // Limpiar Display #define LCD_RETHOM 0x02 // Cursor a inicio de línea 1 #define LCD_LINE1 0x80 // Línea 1 posición 0 #define LCD_LINE2 0xC0 // Línea 2 posición 0 #define LCD_DDRAM 0x80 // Dirección 0x00 de DDRAM #define LCD_CGRAM 0x40 // Dirección 0x00 de CGRAM #define LCD_CURSOR 0x0E // Mostrar solo Cursor #define LCD_BLINK 0x0D // Mostrar solo Blink #define LCD_CURBLK 0x0F // Mostrar Cursor + Blink #define LCD_NOCURBLK 0x0C // No mostrar ni Cursor ni Blink //**************************************************************************** // PROTOTIPOS DE FUNCIONES //**************************************************************************** void lcd_init(void); // Inicializa el LCD void lcd_puts(char * s); // Envía una cadena ram al LCD void lcd_gotorc(char r, char c); // Cursor a fila r, columna c void lcd_clear(void); // Limpia el LCD y regresa el cursor al inicio void lcd_data(char dat); // Envía una instrucción de dato al LCD void lcd_cmd(char com); // Envía una instrucción de comando al LCD char lcd_read(char RS); // Lee un dato del LCD void lcd_write(char inst, char RS); // Escribe una instrucción en el LCD void lcd_nibble(char nibble); void ldelay_ms(unsigned char ); /****************************************************************************** * FileName: lcd.c * Purpose: Librería de funciones para controlar un display LCD con chip * Hitachi HD44780 o compatible. La interface es de 4 bits. * Processor: ATmel AVR * Compiler: IAR-C y AVR-GCC (WinAVR) * Author: Shawn Johnson. http://www.cursomicros.com. * * Copyright (C) 2008 - 2012 Shawn Johnson. All rights reserved. * * License: Se permiten el uso y la redistribución de este código con * modificaciones o sin ellas, siempre que se mantengan esta * licencia y las notas de autor y copyright de arriba. *****************************************************************************/ #include "lcd.h" //**************************************************************************** // Ejecuta la inicialización software completa del LCD. La configuración es // de: interface de 4 bits, despliegue de 2 líneas y caracteres de 5x7 puntos. //**************************************************************************** void lcd_init(void) { /* Configurar las direcciones de los pines de interface del LCD */ lcd_DATAddr |= 0xF0; lcd_CTRLddr |= (1< 40 ms lcd_nibble(0x30); // Function Set: 8-bit ldelay_ms(5); // > 4.1 ms
lcd_nibble(0x30); // Function Set: 8-bit ldelay_ms(1); // > 100 µs lcd_nibble(0x30); // Function Set: 8-bit ldelay_ms(1); // > 40 µs lcd_nibble(0x20); // Function Set: 4-bit ldelay_ms(1); // > 40 µs lcd_nibble(0x20); // Function Set: 4-bit, 2lines, 4×7font lcd_nibble(0x80); // lcd_write(0x0C, 0); // Display ON/OFF Control: Display on, Cursor off, Blink off lcd_write(0x01, 0); // Clear Display lcd_write(0x06, 0); // Entry Mode Set } //**************************************************************************** // Escribe una instrucción en el LCD: // Si RS = 0 la instrucción es de comando (Function Set, Entry Mode set, etc). // Si RS = 1 la instrucción es de dato y va a la DDRAM o CGRAM. //**************************************************************************** void lcd_write(char inst, char RS) { while(lcd_read(0)&0x80); // Esperar mientras LCD esté ocupado if(RS) lcd_CTRLout |= (1< 140 ns lcd_CTRLout |= (1< 450 ns lcd_CTRLout &= ~(1< 140 ns lcd_CTRLout |= (1<
delay_us(2); high = lcd_DATAin; lcd_CTRLout &= ~(1<>4);
// // // // // // // // //
Data Delay Time > 1320 ns Leer nibble alto Para que el LCD prepare el nibble bajo Enable cycle time > 1200 ns Habilitar LCD Data Delay Time > 1320 ns Leer nibble bajo Juntar nibbles leídos
} //**************************************************************************** // Envían cadenas RAM terminadas en nulo al LCD. //**************************************************************************** void lcd_puts(char * s) { unsigned char c, i=0; while(c = s[i++]) lcd_write(c, 1); // Instrucción 'Write Data to DDRAM/CGRAM' } //**************************************************************************** // Ubica el cursor del LCD en la columna c de la línea r. //**************************************************************************** void lcd_gotorc(char r, char c) { if(r==1) r = LCD_LINE1; else r = LCD_LINE2; lcd_write(r+c-1, 0); // Instrucción 'Set DDRAM Address' } //**************************************************************************** // Limpia la pantalla del LCD y regresa el cursor a la primera posición // de la línea 1. //**************************************************************************** void lcd_clear(void) { lcd_write(LCD_CLEAR, 0); // Instrucción 'Clear Display' } //**************************************************************************** // Envían instrucciones de comando y de datos al LCD. //**************************************************************************** void lcd_cmd(char com) { lcd_write(com, 0); // Cualquier instrucción de comando } void lcd_data(char dat) { lcd_write(dat, 1); // Instrucción 'Write Data to DDRAM/CGRAM' } //**************************************************************************** // Genera un delay de n milisegundos //**************************************************************************** void ldelay_ms(unsigned char n) { while(n--) delay_us(1000); }
Prácticas con LCD “Hello World” Mostrar un mensaje de “Hello World” en el LCD es un programa casi tan trillado como hacer parpadear un LED. Según la configuración por defecto de la librería para el LCD, debemos usar la conexión mostrada en el esquema de abajo. La configuración de puertos y de pines a usar se puede cambiar en archivo lcd.h. El pin VEE (o Vo) del LCD establece el contraste de la pantalla. Muchas veces se prefiere quitar el potenciómetro y conectar VEE a tierra para fijar el máximo contraste. En los siguientes circuitos haremos algo parecido.
Circuito de la práctica. /****************************************************************************** * FileName: main.c * Purpose: LCD - Visualización de texto * Processor: ATmega164P * Compiler: IAR-C y AVR-GCC (WinAVR) * Author: Shawn Johnson. http://www.cursomicros.com. *
* Copyright (C) 2008 - 2012 Shawn Johnson. All rights reserved. * * License: Se permiten el uso y la redistribución de este código con * modificaciones o sin ellas, siempre que se mantengan esta * licencia y las notas de autor y copyright de arriba. *****************************************************************************/ #include "avr_compiler.h" #include "lcd.h" void delay_ms(unsigned int t) { while(t--) delay_us(1000); } int main(void) { lcd_init(); while(1) { lcd_gotorc(1,7); lcd_puts("Hello"); lcd_gotorc(2,7); lcd_puts("World"); delay_ms(600); lcd_clear(); delay_ms(400);
// Inicializar LCD
// // // //
Cursor a fila 1 posición 7 Escribir Hello Cursor a fila 2 posición 7 ...
// Pausa de 600 ms // Limpiar pantalla // ...
} }
Visualización de Números Los LCDs solo entienden de caracteres alfanuméricos y algunos otros, pero no saben reconocer números. En esta práctica veremos cómo hacerlo implementando un sencillo reloj. No será el más preciso, pero servirá de buen ejemplo parar formatear números. Para el circuito, de ahora en adelante, en vez del potenciómetro, colocaremos un diodo 1N4148 en el pin VEE para fijar la tensión (VDD-VEE) a cerca de 4.3 V. En la mayoría de los LCDs este valor brinda un muy aceptable nivel de contraste de la pantalla.
Circuito de la práctica. La función lcd_puts recibe como parámetro un array de tipo char, que en su forma más usada sería una cadena texto. Para visualizar números en el LCD primero debemos convertirlos en cadenas de texto. La conocida función sprintf (acrónimo de string print formatted) puede formatear cadenas y números en diferentes bases y colocarlas en el array que recibe como primer parámetro. Sprintf está basada en printf, así que tiene las mismas características y limitaciones. En este programa solo se convierten números enteros. Pero si deseas utilizar números de punto flotante tendrás que habilitar el uso de la librería que contiene printf en versión completa. Para más información puedes revisar la sección Configuración de printf de la clase de Studio5 y WinAVR. Sprintf soporta varios formatos de números e incluso en su modo básico requiere de cierta memoria que a veces podría ser de consideración. Para ese caso también se pueden usar otras funciones de la librería C estándar stdlib.h, como itoa, por ejemplo. Normalmente no las uso porque tienen variaciones entre los compiladores y al menos para las prácticas como ésta prefiero no tocar esas divergencias. /****************************************************************************** * FileName: main.c * Purpose: LCD - Visualización de números * Processor: ATmega164P
* Compiler: IAR-C y AVR-GCC (WinAVR) * Author: Shawn Johnson. http://www.cursomicros.com. * * Copyright (C) 2008 - 2012 Shawn Johnson. All rights reserved. * * License: Se permiten el uso y la redistribución de este código con * modificaciones o sin ellas, siempre que se mantengan esta * licencia y las notas de autor y copyright de arriba. *****************************************************************************/ #include "avr_compiler.h" #include "lcd.h" void delay_ms(unsigned int t) { while(t--) delay_us(1000); } int main(void) { char buff[17]; unsigned seg, min, hor; seg = min = 0; hor = 12; lcd_init(); lcd_gotorc(1,4); lcd_puts("easy clock");
// Array de 17 elementos tipo char
// Inicializar LCD // Cursor a fila 1 posición 4
for(;;) { sprintf(buff, "%2d:%02d:%02d ", hor, min, seg); // Formatear lcd_gotorc(2,5); // Cursor a fila 2 posición 5 lcd_puts(buff); // Enviar buffer a LCD if(++seg > 59) { seg = 0; if(++min > 59) { min = 0; if(++hor > 12) hor = 1; } } delay_ms(998); } }
Caracteres gráficos en LCD La creación de caracteres gráficos puede ser un tema superfluo. Aun así, suponiendo que no faltarán algunas personas obsesivas como yo, que siempre quieren probarlo todo, he preparado esta práctica para ir cerrando la clase.
Hagamos un poco de memoria. Cuando enviamos el código de un carácter alfanumérico a la DDRAM del LCD, su chip interno buscará en la CGROM el patrón correspondiente y luego lo visualizará en la pantalla. Así se escriben todos textos (y así hemos trabajado hasta ahora). Ahora bien, si el código enviado vale entre 0x00 y 0x07 (o 0x08 y 0x0F), el chip interno buscará su patrón de visualización en la CGRAM. Siendo ésta una RAM de lectura/escritura, podemos programar en ella los diseños que se nos ocurran.
Mapa de memoria para la creación de nuevos caracteres. La CGRAM (Character Generator RAM) consta de 64 bytes en los que se pueden escribir los patrones de 8 nuevos caracteres de 5×7 puntos ó 4 caracteres de 5×10 puntos. Aquí veremos el primer caso. Cuando los caracteres son de 5×7 puntos los 64 bytes se dividen en 8 bloques de 8 bytes cada uno, y cada bloque almacena el patrón de un nuevo carácter. El esquema mostrado arriba indica que:
El primer bloque de CGRAM, con direcciones desde 0b00000000 hasta 0b00000111, corresponde al código 0x00 (ó 0x80) de la DDRAM.
El segundo bloque CGRAM, con direcciones desde 0b00001000 hasta 0b00001111, corresponde al código 0x01 (ó 0x88) de la DDRAM; y así sucesivamente.
Por ejemplo, la figura de arriba indica que se han rellenado los dos primeros bloques con los patrones de dos pacman. Hasta ahí solo se han creado dos nuevos caracteres. Para mostrarlos en el LCD habría que escribir un código así: lcd_data(0x00); lcd_data(0x01);
// Visualizar primer pacman // Visualizar segundo pacman
Sobre la práctica en sí, como parte de su funcionalidad, el LCD tiene instrucciones para desplazar lo mostrado en la pantalla hacia un lado u otro. Puede parecer interesante, pero sus limitaciones llevan a muchos a realizar esos efectos mediante rutinas software. Pues es lo que haremos en esta práctica, mostrar por el LCD un mensaje que pasa como una marquesina, y como nuevo carácter pondremos a un pacman glotón que en la esquina inferior izquierda.
Circuito de la práctica. Después de iniciado el LCD, los datos que se le envíen irán a la DDRAM (para mostrar caracteres en la pantalla). Como los patrones de los pacman deben ir en la CGRAM necesitamos establecerla como destino. Para eso enviamos el comando Set CGRAM Address con la dirección de CGRAM que queremos acceder.
La otra sentencia lcd_cmd(LCD_CGRAM+8) permitirá que los siguientes datos vayan al segundo bloque (de 8 bytes) de la CGRAM. Fíjate en que no era necesario porque el Puntero de RAM ya estaba apuntando a esta dirección. Como hemos creado los dos pacman en los dos primeros bloques (de 8 bytes) de la CGRAM, los códigos para accederlos serán 0 (PacOpen) y 1 (PacShut), respectivamente. A continuación se encuentra la sentencia lcd_clear(). Con ella no solo limpiamos la pantalla del LCD (que, por cierto, ya estaba limpia) sino que volvemos a cambiar a la DDRAM. Por si no quedó claro cómo se forman los patrones de los dos pacman, aquí los tenemos solitos. (Los bits × no importan, pueden ser 1s o 0s.)
const char PattOpen [] = {0x0F,0x1C,0x18,0x10,0x18,0x1C,0x0F,0x00}; //Pattern const char PattShut [] = {0x00,0x0E,0x1F,0x10,0x1F,0x0E,0x00,0x00}; //Pattern
Pasando a otros temas: el texto de la pantalla se desplaza una posición cada 400 ms. Si te parece que avanza muy lento, puedes disminuir esta pausa. No obstante, podrías empezar a ver como si hubiera dos letras por casillero de la pantalla. Ello se debe a que el carácter enviado al LCD no se muestra ni se borra de inmediato. Es lo que sus datasheets llaman tiempo de respuesta de visualización. En general, a diferencia del Basic, en C es muy mal visto el uso de un goto, salvo un caso extremo. goto funciona como en el ensamblador: salta a otro punto del programa, identificado con una etiqueta. Mi goto salta a la etiqueta start para salir de dos bucles al mismo tiempo. Dicen que ése es uno de los pocos casos considerados extremos: salir intempestivamente de varios bucles anidados. A decir verdad, siempre hay algoritmos alternativos para evitar el goto. /****************************************************************************** * FileName: main.c * Purpose: LCD - Creación de caracteres gráficos personalizados * Processor: ATmega164P * Compiler: IAR-C y AVR-GCC (WinAVR) * Author: Shawn Johnson. http://www.cursomicros.com. * * Copyright (C) 2008 - 2012 Shawn Johnson. All rights reserved. * * License: Se permiten el uso y la redistribución de este código con * modificaciones o sin ellas, siempre que se mantengan esta * licencia y las notas de autor y copyright de arriba. *****************************************************************************/ #include "avr_compiler.h"
#include "lcd.h" #define #define #define
LCD_LEN PacOpen PacShut
16 0x00 0x01
// Para LCD de 2×16 // Indentificador de carácter nuevo // Indentificador de carácter nuevo
PROGMEM char Taine[] = " \"EL HAMBRE PRODUCE POEMAS INMORTALES. \ LA ABUNDANCIA, SOLAMENTE INDIGESTIONES Y TORPEZAS\" "; void delay_ms(unsigned int t) { while(t--) delay_us(1000); } int main(void) { unsigned char j; // Índice relativo unsigned char i; // Índice base char c; const char PattOpen[] = {0x0F,0x1C,0x18,0x10,0x18,0x1C,0x0F,0x00}; const char PattShut[] = {0x00,0x0E,0x1F,0x10,0x1F,0x0E,0x00,0x00}; lcd_init(); /* Crear dos nuevos caracteres (los pacman's) en la CGRAM */ lcd_cmd(LCD_CGRAM); // Instrucción Set CGRAM Address for (i=0; i<8; i++) // Volcar patrón de pacman 1 lcd_data(PattOpen[i]); // lcd_cmd(LCD_CGRAM + 8); for (i=0; i<8; i++) lcd_data(PattShut[i]);
// Instrucción Set CGRAM Address // Volcar patrón de pacman 2 //
lcd_clear(); lcd_puts(" Hungry Pacman ");
// Limpiar pantalla y regresar a DDRAM // Escribir "Hungry Pacman" en LCD
while(1) { start: i = 0; for(;;) { lcd_cmd(LCD_LINE2); if(i & 0x01) lcd_data(PacOpen); else lcd_data(PacShut);
// // // // //
Cursor Si bit enviar Si no, enviar
a inicio de línea 2 0 de i es 1, pacman abierto pacman cerrado
for(j=0; j
}
Comunicación PC – AVR – LCD Aquí tenemos un programa cliché en los ejemplos de interface entre un microcontrolador y un ordenador mediante el puerto serie. El programa terminal envía por el puerto serie las letras que presionemos en el teclado. El AVR los recibirá, los reflejará al PC y también los visualizará en el display LCD. Haremos que un par de teclas generen instrucciones especiales:
La tecla Escape, de código 27, sirve pare limpiar la pantalla del LCD. La tecla Retroceso o Backspace, de código 0x08 = „\b‟, lleva el cursor del LCD una posición atrás.
En esta ocasión será de utilidad tener el cursor a la vista.
Circuito de la práctica. /****************************************************************************** * FileName: main.c * Purpose: LCD - Acceso a LCD desde PC mediante AVR * Processor: ATmega164P * Compiler: IAR-C y AVR-GCC (WinAVR) * Author: Shawn Johnson. http://www.cursomicros.com. * * Copyright (C) 2008 - 2012 Shawn Johnson. All rights reserved. * * License: Se permiten el uso y la redistribución de este código con * modificaciones o sin ellas, siempre que se mantengan esta * licencia y las notas de autor y copyright de arriba. *****************************************************************************/ #include "avr_compiler.h" #include "lcd.h" #include "usart.h" int main(void) { lcd_init(); lcd_cmd(LCD_CURBLK);
// Ver interface en "lcd.h" // Mostrar Cursor + Blink
usart_init(); // 9600 - 8N1 puts("\n\r Acceso a LCD desde PC \n\r"); puts("\n\r Escribe en el LCD... \n\r"); while(1) { if(kbhit()) // Si hay algún dato en el buffer de recepción { char c = getchar(); // Obtener un dato putchar(c); // Devolver dato switch(c) { case 0x1B: lcd_clear(); // Limpiar LCD puts("\n\r"); break; case 0x08: lcd_cmd(0x10); // Cursor atrás lcd_data(' '); // Escribir espacio blanco lcd_cmd(0x10); // Cursor atrás break; default: lcd_data(c); // Escribir c en LCD } } } }
Las Interrupciones en los AVR
Introducción
Hay una analogía que siempre recuerdo desde que la leí en un buen libro de Turbo Pascal cuando aprendía a programar en dicho lenguaje. Cuando vamos a recibir una visita en nuestra casa podemos ir a la puerta a cada momento para ver si ya llegó y atenderla apropiadamente, o podemos quedarnos haciendo nuestras labores cotidianas esperando a que sea la visita quien llame a la puerta para ir a recibirla. Ir a la puerta constantemente se compara por ejemplo con testear los puertos del AVR para ver si se presionó algún pulsador o algún teclado y actuar en consecuencia. Eso se conoce como técnica Polling o de sondeo e implica el desperdicio de recursos y ciclos de CPU. En este capítulo aprenderemos a atender nuestras visitas justo cuando llamen a la puerta para que el AVR no se canse en vano y que se ponga a “dormir”, si fuera posible. Ésta es solo una pequeña muestra de lo que se puede conseguir con las interrupciones.
¿Qué son las Interrupciones? Una interrupción es una llamada “inesperada”, urgente e inmediata a una función especial denominada Interrupt Service Routine (ISR). El mecanismo funciona así: sin importar lo que esté haciendo en main o cualquier función relacionada con main, cuando ocurra la interrupción el CPU hará una pausa y pasará a ejecutar el código de ISR. Tras terminarlo, el CPU regresará a la tarea que estaba realizando antes de la interrupción, justo donde la había suspendido. Aunque es posible provocar interrupciones desde el programa ejecutándolas como si fueran funciones ordinarias, las interrupciones son disparadas (llamadas) por eventos del hardware del microcontrolador. El evento puede ser algún cambio en cierto pin de E/S, el desbordamiento de un Timer, la llegada de un dato serial, etc. Se puede deducir por tanto que las fuentes de interrupción están relacionadas con la cantidad de recursos del microcontrolador.
Los Vectores de Interrupción Cada interrupción está identificada por un Vector de Interrupción, que no es otra cosa que una dirección particular en la memoria FLASH. Todos los Vectores están ubicados en posiciones consecutivas de la memoria FLASH y forman la denominada Tabla de Vectores de Interrupción. El RESET no es una interrupción pero su dirección 0x0000 también se conoce como Vector de Reset. Por defecto, la Tabla de Vectores de Interrupción está ubicada en las primeras posiciones de la memoria, tal como se ve abajo. Solo cuando se habilita el uso de la Sección de Boot Loader toda la tabla se desplazará al inicio de dicha sección. Enseguida se presenta una tabla con las 35 interrupciones posibles en los ATmegaNN4YY. Debemos recordar que solo los ATmega1284yy tienen el Timer3 y por tanto las 4 interrupciones relacionadas con el Timer3 no estarán disponibles en los otros ATmega de esta serie. Aprenderemos de a poco y para empezar en esta clase nos ocuparemos de las 7 interrupciones externas, desde INT0 hasta PCINT3. Las restantes serán estudiadas en sus módulos respectivos. Num Dirección de Nombre de Fuente de interrupción Vector Programa Vector de Interrupción 1 0x0000 RESET External Pin, Power-on Reset, Brown-out Reset,
Num Dirección de Nombre de Vector Programa Vector de Interrupción 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
0x0002 0x0004 0x0006 0x0008 0x000A 0x000C 0x000E 0x0010 0x0012 0x0014 0x0016 0x0018 0x001A 0x001C 0x001E 0x0020 0x0022 0x0024 0x0026 0x0028 0x002A 0x002C 0x002E 0x0030 0x0032 0x0034 0x0036 0x0038 0x003A 0x003C 0x003E 0x0040 0x0042 0x0044
INT0 INT1 INT2 PCINT0 PCINT1 PCINT2 PCINT3 WDT TIMER2_COMPA TIMER2_COMPB TIMER2_OVF TIMER1_CAPT TIMER1_COMPA TIMER1_COMPB TIMER1_OVF TIMER0_COMPA TIMER0_COMPB TIMER0_OVF SPI_STC USART0_RX USART0_UDRE USART0_TX ANALOG_COMP ADC EE_READY TWI SPM_READY USART1_RX USART1_UDRE USART1_TX TIMER3_CAPT TIMER3_COMPA TIMER3_COMPB TIMER3_OVF
Fuente de interrupción Watchdog Reset, and JTAG AVR Reset External Interrupt Request 0 External Interrupt Request 1 External Interrupt Request 2 Pin Change Interrupt Request 0 Pin Change Interrupt Request 1 Pin Change Interrupt Request 2 Pin Change Interrupt Request 3 Watchdog Time-out Interrupt Timer/Counter2 Compare Match A Timer/Counter2 Compare Match B Timer/Counter2 Overflow Timer/Counter1 Capture Event Timer/Counter1 Compare Match A Timer/Counter1 Compare Match B Timer/Counter1 Overflow Timer/Counter0 Compare Match A Timer/Counter0 Compare match B Timer/Counter0 Overflow SPI Serial Transfer Complete USART0 Rx Complete USART0 Data Register Empty USART0 Tx Complete Analog Comparator ADC Conversion Complete EEPROM Ready 2-wire Serial Interface Store Program Memory Ready USART1 Rx Complete USART1 Data Register Empty USART1 Tx Complete Timer/Counter3 Capture Event Timer/Counter3 Compare Match A Timer/Counter3 Compare Match B Timer/Counter3 Overflow
Para entender cómo funciona el mecanismo de las interrupciones en bajo nivel, recordemos que el Contador de Programa es un registro que dirige cada una de las instrucciones que ejecuta el CPU. Pues bien, al dispararse la interrupción el hardware guardará en la Pila el valor actual del Contador de Programa y lo actualizará con el valor del Vector de Interrupción respectivo, de modo que el CPU pasará a ejecutar el código que se encuentre a partir de esa dirección. Al final del código de la interrupción debe haber una instrucción de retorno que restaure el Contador de Programa con el valor que se había guardado en la Pila. La instrucción es reti y la pone el compilador. Si se llegara a producir el evento excepcional en que se disparen dos o más interrupciones al mismo tiempo, se ejecutarán las interrupciones en orden de prioridad. Tiene mayor prioridad la interrupción cuyo Vector se ubique más abajo, es decir, entre todas, la interrupción INT0 tiene siempre las de ganar.
La estructura y características de la Tabla de Vectores de Interrupción pueden variar entre las diferentes familias de ATmega y a veces entre diferentes partes de una misma serie. Por ejemplo, los ATmega de la serie 8yy no tienen la interrupción externa INT2 y tampoco las interrupciones PCINT3 (porque les falta el puerto A). Además, el ATmega48yy no dispone de la funcionalidad de Boot Loader, así que este AVR no puede desplazar su Tabla de Vectores de Interrupción. La ausencia de algunas interrupciones hace que los otros Vectores cambien de valor. En cualquier caso, para nosotros, los programadores en C o Basic, es suficiente tener en cuenta los nombres de los Vectores de Interrupción, que en la tabla de arriba se resaltan con enlaces en en azul. Los nombres de los Vectores de Interrupción presentados corresponden al datasheet y no necesariamente son idénticos a los que utilizan los compiladores AVR GCC o IAR C. Estos nombres se encuentran definidos en los archivos de dispositivo de cada AVR, ubicados en la carpeta include de cada compilador. La instalación por defecto de WinAVR con Studio 5 en Windows 7 marca la ruta C:\Program Files (x86)\Atmel\AVR Studio 5.0\AVR Toolchain\avr\include\avr. Allí los puedes ubicar, y de hecho es recomendable examinarlos que de vez en cuando. Pero si de momento deseas ahorrarte el trabajo te diré que la única diferencia es el apéndice _vect. Es decir, en todos los archivos de dispositivo de IAR C y de AVR GCC (en sus versiones actuales) los nombres de los Vectores de Interrupción son los mismos que aparecen en el datasheet pero con el añadido _vect, como se muestra en la siguiente tabla de ejemplo. Está de más decir que en nuestros programas debemos usar la forma con _vect. Nombre de Nombre de Vector de Interrupción Vector de Interrupción en datasheet en archivo de dispositivo INT0 INT0_vect INT1 INT1_vect INT2 INT2_vect PCINT0 PCINT0_vect PCINT1 PCINT1_vect PCINT2 PCINT2_vect PCINT3 PCINT3_vect TIMER0_COMPA TIMER0_COMPA_vect TIMER0_COMPB TIMER0_COMPB_vect TIMER0_OVF TIMER0_OVF_vect USART0_RX USART0_RX_vect USART0_UDRE USART0_UDRE_vect USART0_TX USART0_TX_vect Lamentablemente para quienes programan en CodeVisionAVR, Pavel Haiduc decidió –no sé por qué– usar otros nombres para los Vectores de Interrupción. No solo son diferentes de los indicados en los datasheets sino que la Tabla de Vectores empieza en 2 y no en 1 (sin incluir el Vector de reset, claro está). Así que ellos no tendrán más remedio que recurrir a los archivos de dispositivo de sus AVR, los cuales se hallan en la fácilmente ubicable carpeta inc creada por CodeVisionAVR en su directorio de instalación.
Las Funciones de Interrupción
La Función de Interrupción o ISR va siempre identificada por su Vector de Interrupción, y su esquema varía ligeramente entre un compilador y otro, puesto que no existe en el lenguaje C un formato estándar. Lo único seguro es que es una función que no puede recibir ni devolver ningún parámetro. En el compilador AVR GCC (WinAVR) la función de interrupción se escribe utilizando la palabra reservada ISR. Recordemos que el Vector_de_Interrupcion debe tener la terminación _vect, como se indicó anteriormente, y si tienes dudas puedes buscar en la carpeta include del directorio de instalación de WinAVR. ISR (Vector_de_Interrupcion) { // Código de la función de interrupción. // No requiere limpiar el flag respectivo. El flag se limpia por hardware }
Por otro lado, en IAR C y CodeVisionAVR la construcción es un poquito más elaborada. Requiere el empleo de la directiva #pragma vector y la palabra reservada __interrupt. El Nombre_de_Interrupcion queda a la libertad del programador. #pragma vector = Vector_de_interrupcion __interrupt void Nombre_de_Interrupcion (void) { // Código de la función de interrupción. // No requiere limpiar el flag respectivo. El flag se limpia por hardware }
Por fortuna, estas divergencias entre IAR C y WinAVR solo se presentan en el encabezado de la función. La implementación del cuerpo de la función es idéntica en ambos compiladores. Además existe la posibilidad de utilizar macros que adapten el esquema de la función de interrupción de IAR C al formato de WinAVR. Estas macros están escritas en el archivo avr_compiler.h del Atmel Software Framework o ASF y que siempre se usa en los programas de cursomicros.com. En realidad, CodeVision AVR ofrece otras formas adicionales de implementar una función de interrupción pero, por más que se parezcan en forma, lamentablemente ninguna de ellas es compatible con el código de IAR C o WinAVR, debido a las diferencias en los nombres de los Vectores de Interrupción.
Control de las Interrupciones Hay dos tipos de bits para controlar las interrupciones: los Bits Enable, que habilitan las interrupciones, y los Bits de Flag, que indican cuál interrupción se ha producido. Bueno, eso para decirlo a grandes rasgos. Hay un bit enable individual para cada interrupción y además hay un bit enable general I (ubicado en el registro SREG) que afecta a todas las interrupciones. Para habilitar una interrupción hay que setear su bit enable individual como el bit enable general I. También se pueden habilitar varias interrupciones del mismo modo. Ninguna habilitación individual tendrá efecto, es decir, no disparará una interrupción si el bit I está en cero. Por otro lado, cada interrupción tiene un Bit de Flag único, que se setea automáticamente por hardware cuando ocurre el evento de dicha interrupción. Eso pasará independientemente de si la interrupción está habilitada o no. Si la interrupción fue previamente habilitada, por supuesto que se disparará.
Cada interrupción habilitada y disparada, saltará a su correspondiente Función de Interrupción o ISR, de modo que a diferencia de algunos otros microcontroladores no será necesario sondear los flags de interrupción para conocer la fuente de interrupción. Los AVR van más lejos y tienen un hardware que limpia automáticamente el bit de Flag apenas se empiece a ejecutar la función de interrupción. Pero puesto que los flags se habilitan independientemente de si las interrupciones están habilitadas o no, en ocasiones será necesario limpiarlos por software y en ese caso debemos tener la especial consideración de hacerlo escribiendo un uno y no un cero en su bit respectivo. Sí, señor, dije, uno. Al ejecutarse la función de interrupción también se limpia por hardware el bit enable general I para evitar que se disparen otras interrupciones cuando se esté ejecutando la interrupción actual. Sin embargo, la arquitectura de los AVR le permite soportar ese tipo de interrupciones, llamadas recurrentes o anidadas, y si así lo deseamos podemos setear en el bit I dentro de la ISR actual. A propósito, el ya famoso bit enable general I se puede escribir como cualquier otro bit de un registro de E/S. Pero dada su especial importancia, existen dos exclusivas instrucciones de ensamblador llamadas sei (para setear I) y cli (para limpiar I). El archivo avr_compiler.h ofrece las macros sei() y cli() para llegar a esas instrucciones.
Las Interrupciones INT0, INT1 e INT2 Los ATmega de 40 pines tienen las tres interrupciones y los ATmega de 28 pines solo tienen las dos primeras. Como las tres interrupciones tienen características idénticas y se controlan igual, la descripción será general citando una x que representa el 0, 1 ó 2. El evento que puede disparar la interrupción INTx es un flanco (de subida y/o de bajada) o un nivel bajo detectado en el pin INTx del AVR, sin importar si ese pin está configurado como entrada o como salida. Los bits de enable y flag de estas interrupciones se encuentran en los registros
EIMSK = External interrupts Mask Register. Contiene los bits enable. EIFR = External Interrupts Flags Register. Contiene los bits de flag. EICRA = External Interrupts Control Register A. Configura la señal externa que va a generar la interrupción. Es A porque hay AVRs más grandes con más interrupciones INTx donde además existe el registro EICRB.
EIMSK---------------INT2INT1INT0 EIFR---------------INTF2INTF1INTF0 EICRA------ISC21ISC20ISC11ISC10ISC01ISC00 Para habilitar la interrupción INTx hay que setear el bit INTx, del registro EIMSK, además del bit enable general I, del registro SREG. Una vez producido el evento, el hardware seteará el flag INTFx, del registro EIFR, y luego se disparará la interrupción. Este evento se debe configurar previamente en el registro EICRA y hay cuatro opciones posibles. Modo
ISCx1
ISCx0
Evento de la Interrupción
Modo 0 1 2 3
ISCx1
ISCx0
0
0
0 1 1
Evento de la Interrupción Nivel bajo en el pin INTx.
1
Cualquier flanco (de subida y/o de bajada) detectado en el pin INTx.
0
Flanco de bajada detectado en el pin INTx.
1
Flanco de subida detectado en el pin INTx.
EICRA------ISC21ISC20ISC11ISC10ISC01ISC00
Como la mayoría de los registros, EICRA inicia con todos sus bits a cero lo que significa que por defecto la interrupción INTx habilitada se disparará cuando dicho pin esté a nivel bajo. La interrupción externa INTx tiene la capacidad de “despertar” al AVR, es decir, de sacarlo del modo sleep. Ésta es una característica muy notable que veremos luego.
Práctica: Uso de la interrupción INTx En estas prácticas de ejemplo evitaremos programas sofisticados con códigos grandes que desvíen la atención hacia una breve aplicación de la teoría expuesta. Por eso no nos vendrá mal volver a los socorridos LEDs parpadeantes. El programa tendrá dos tareas: la rutina principal se encargará de parpadear un LED y la función de interrupción hará bascular otro LED cada vez que presionemos un pulsador. Esto será como fusionar dos programas que alguna vez hicimos. “Correr dos programas a la vez…” Dicen que algo así le paso por la cabeza a Bill Gates cuando pensó en MS Windows. De las señales que se generan al presionar el botón escogeremos el flanco de bajada para disparar la interrupción INT0.
Circuito de la práctica. El código fuente
Cada aplicación puede tener sus propias especificaciones, pero, en general, un buen hábito de programación es poner la sentencia sie(); que setea el bit I del registro SREG cuando ya todo esté listo para atender a la interrupción. Al analizar la estructura del programa, notamos que la función ISR es totalmente independiente de main, es decir, no es referenciada desde ningún punto de main. Una vez habilitada, la interrupción se disparará cuando alguien presione el botón (en el flanco de bajada). En ese preciso instante (quizá cuando se esté ejecutando PINC = 0x02 o quizá en algún punto dentro de delay_ms(600)) el CPU pasará a ejecutar la función ISR. Al salir de ISR, el CPU regresará a continuar la tarea que estaba ejecutando antes de la interrupción. /****************************************************************************** * FileName: main.c * Purpose: Uso de la interrupción INTx * Processor: ATmega AVR * Compiler: IAR-C y AVR-GCC (WinAVR) * Author: Shawn Johnson. http://www.cursomicros.com. * * Copyright (C) 2008 - 2012 Shawn Johnson. All rights reserved. * * License: Se permiten el uso y la redistribución de este código con * modificaciones o sin ellas, siempre que se mantengan esta * licencia y las notas de autor y copyright de arriba. *****************************************************************************/ #include "avr_compiler.h" void delay_ms(unsigned int t) {
while(t--) delay_us(1000); } //**************************************************************************** // Interrupt Service Routine, ISR // Esta función se ejecuta cuando se detecta un flanco de bajada en el pin INT0 //**************************************************************************** ISR (INT0_vect) { PINC = 0x01; // Conmutar pin PC0 delay_ms(40); // Para pasar los rebotes } //**************************************************************************** // Función principal //**************************************************************************** int main(void) { DDRC = 0x03; // Pines PC0 y PC1 para salida (LEDs) PORTD = 0x04; // Habilitar pull-up de pin PD2/INT0 (pulsador) /* Habilitar y configurar la interrupción INT0 para que se dispare con * cada flanco de bajada detectado en el pin INT0 (PD2) */ EIMSK = (1<
// Habilitación general de interrupciones
while(1) { PINC = 0x02; delay_ms(600); }
// Bucle infinito // Conmutar pin PC1 // Pausa de 600ms
}
El Modo Sleep El modo Sleep es un estado en que se detiene el oscilador del sistema y, por tanto, dejan de funcionar todas las partes del microcontrolador que dependen de él, incluyendo en algunos casos el mismo procesador, es decir, se “congela” la ejecución del programa. Sin embargo los valores de todos los registros y puertos del microcontrolador permanecerán inalterables. En este estado se dice que el microcontrolador está durmiendo, por el término sleep = sueño, en inglés. La pregunta es ¿para qué sirve un microcontrolador con su hardware congelado? Pues hay aplicaciones donde el microcontrolador debe atender ciertas tareas solo cuando ocurre un evento externo como por ejemplo la pulsada de un botón. El resto del tiempo no hace nada útil. Al hacer que el microcontrolador se ponga a dormir y que despierte solo cuando cierto evento se lo demande, se consigue ahorrar muchísima energía que se perdería con el CPU y demás periféricos estando activos en vano. Esto es clave sobre todo en circuitos alimentados por baterías. Los microcontroladores AVR tienen un sistema oscilador sofisticado que divide el reloj en varias ramificaciones que van a los diferentes módulos del AVR. De esa forma, su modo sleep tiene hasta 6
diferentes niveles dependiendo de las ramificaciones del reloj que se pongan a congelar. Detallar cada una de ellas en este momento extendería tanto el tema que movería el enfoque de las interrupciones. Solo diré que el mayor nivel, es decir, donde el hardware del AVR se congela por completo se denomina Power-down. El modo Power-down se configura escribiendo el valor 0x02 en el registro SMCR y luego solo bastará con ejecutar la instrucción de ensamblador sleep para que el AVR “cierre sus ojos”. El modo sleep es muy propio de los microcontroladores y no existe en el lenguaje C una sentencia para la instrucción sleep. Cada compilador la implementa a su modo. En el archivo avr_compiler.h original se utiliza la macro sleep_enter(), pero yo le añadí otra definida como sleep(), a secas. Tocamos el modo sleep ahora porque el evento por excelencia que puede despertar al microcontrolador es el disparo de una interrupción proveniente de una parte del microcontrolador que no esté congelado. Puesto que las interrupciones INTx y PCINTx son externas, ellas pueden sacar al AVR incluso del sueño más profundo, o sea del modo Power-down. Cuando se dispare una interrupción lo primero que hará el CPU al despertar es ejecutar la primera instrucción de ensamblador que sigue a sleep, e inmediatamente después pasará a ejecutar la función de interrupción ISR. Si aún recuerdas el retardo de arranque, te diré que es aquí donde entra en acción: esto es, después de descongelarse el oscilador del sistema, habrá un tiempo llamado retardo de arranque en que el AVR espera en estado de RESET hasta que el oscilador se haya estabilizado por completo. Luego, recién, el CPU reiniciará su trabajo. De los 6 modos sleep del AVR, el retardo de arranque solo actúa en los niveles Power- down y Power-save porque en los demás niveles, el oscilador del AVR no está del todo detenido.
Práctica: Interrupciones Múltiples + Modo Sleep Si al programa anterior le quitásemos la tarea de la rutina principal, el AVR ya no tendría nada que hacer allí. Éste puede ser un buen momento para tomar una siesta. Por otro lado, en esta ocasión experimentaremos con las tres interrupciones, INT0, INT1 e INT2, al mismo tiempo como ejemplo de uso de interrupciones múltiples.
Circuito de la práctica. El código fuente /****************************************************************************** * FileName: main.c * Purpose: Uso de las interrupciones INTx + Modo Sleep * Processor: ATmega164P * Compiler: IAR-C y AVR-GCC (WinAVR) * Author: Shawn Johnson. http://www.cursomicros.com. * * Copyright (C) 2008 - 2012 Shawn Johnson. All rights reserved. * * License: Se permiten el uso y la redistribución de este código con * modificaciones o sin ellas, siempre que se mantengan esta * licencia y las notas de autor y copyright de arriba. *****************************************************************************/ #include "avr_compiler.h" void delay_ms(unsigned int t) { while(t--) delay_us(1000); } //**************************************************************************** // Gestor de Interrupción INT0 // Esta función se ejecuta cuando se detecta un flanco de bajada o de subida // en el pin INT0 //**************************************************************************** ISR (INT0_vect) { PINC = 0x01; // Conmutar pin PC0 delay_ms(40); // Para pasar los rebotes }
//**************************************************************************** // Gestor de Interrupción INT1 // Esta función se ejecuta cuando se detectan flancos de subida en el pin INT1 //**************************************************************************** ISR (INT1_vect) { PINC = 0x02; // Conmutar pin PC1 delay_ms(40); // Para pasar los rebotes } //**************************************************************************** // Gestor de Interrupción INT2 // Esta función se ejecuta cuando el pin INT2 se encuentra en nivel bajo //**************************************************************************** ISR (INT2_vect) { PINC = 0x04; // Conmutar pin PC2 delay_ms(40); // Para pasar los rebotes } //**************************************************************************** // Función principal //**************************************************************************** int main(void) { DDRC = 0x07; // Pines PC0, PC1 y PC2 para salida (LEDs) /* Habilitar pull-ups de pines INT0/PD2, INT1/PD3 e INT2/PB2 para los * pulsadores. Se asume que estos pines están configurados como entradas */ PORTD = 0x0C; // PORTB = 0x04; // /* Habilitar las interrupciones INT0, INT1 e INT2 y configurarlas para que * se disparen: * INT0 con cada flanco (de bajada o subida) en el pin INT0/PD2 (modo 1) * INT1 con cada flanco de subida en el pin INT1/PD3 (modo 3) * INT2 mientras haya un nivel bajo en el pin INT2/PB2 (modo 0) */ EIMSK = (1<
// Habilitación general de interrupciones
while(1) // Bucle infinito { /* Entrar en modo sleep (Power-Down mode) */ SMCR = (1<
El AVR despertará con el disparo de la interrupción, ejecutará nop() (que también equivale a una instrucción de ensamblador) y luego llamará a la función ISR respectiva. Lo demás es historia conocida. Aunque en algunos casos el nop() es recomendable, en esta ocasión lo puse solo para esta explicación.
Interrupciones de Cambio de Pin, PCINTx Esta interrupción se dispara cada vez que se detecta un cambio de nivel lógico „1‟ a „0‟ o viceversa en cualquiera de los pines de los puertos del AVR, sin importar si están configurados como entrada o como salida. Aunque no es propiamente reconocido, también se podría decir que se dispara con los flancos de subida y de bajada en los pines de puertos. En ese sentido, se parece bastante a las interrupciones INTx. La interrupción de Cambio de Pin también puede sacar al AVR del modo Sleep. Esta interrupción no está presente en los AVR antiguos como los ATmega32, ATmega16, ATmega8535, etc. Aparte del bit enable general I, del registro SREG, las interrupciones de cambio de pin se habilitan pasando por las dos siguientes etapas, no necesariamente en el orden citado. Primero, se debe setear el bit que identifica el puerto donde se encuentran los pines que generarán las interrupciones. Estos son bits de enable ubicados en el registro PCICR (Pin Change Interrupt Control Register). Para los ATmega de 4 puertos la correspondencia es la siguiente. PCICR------------PCIE3PCIE2PCIE1PCIE0
Y para los ATmega de 3 puertos como los de la serie 8xx, la correspondencia entre los bits PCIEx (Pin Change Interrupt Enable) y los puertos del AVR es PCICR---------------PCIE2PCIE1PCIE0
Luego se deben setear los bits enable que identifican individualmente los pines de los puertos. Estos bits se encuentran en los registros de máscara PCMSK (Pin Change Mask Register). Hay un registro de máscara por cada puerto del AVR aunque la relación varía según el número de puertos del AVR, como se indica en la siguiente tabla. ATmega de 4 puertos Registro de máscara Puerto PCMSK0 PORTA PCMSK1 PORTB PCMSK2 PORTC PCMSK3 PORTD
ATmega de 3 puertos Registro de máscara Puerto PCMSK0 PORTB PCMSK1 PORTC PCMSK2 PORTD
Cada bit del registro de máscara PCMSK corresponde a su respectivo pin de PORT. Por ejemplo, si en un AVR de 4 puertos seteamos los bits 4 y 7 de PCMSK2, estaremos habilitando las interrupciones de los pines 4 y 7 de PORTC. Esta correspondencia se cumple incluso en los AVR cuyos puertos no tengan los 8 pines completos. Otra forma de seleccionar los pines de interrupción es ubicándolos directamente por sus nombres PCINTx. Para esto también debes estar revisando el diagrama de pines del AVR. PCMSK0PCINT7PCINT6PCINT5PCINT4PCINT3PCINT2PCINT1PCINT0 PCMSK1PCINT15PCINT14PCINT13PCINT12PCINT11PCINT10PCINT9PCINT8 PCMSK2PCINT23PCINT22PCINT21PCINT20PCINT19PCINT18PCINT17PCINT16 PCMSK3PCINT31PCINT30PCINT29PCINT28PCINT27PCINT26PCINT25PCINT24 Observa que cada bit PCINTx corresponde a un pin del AVR con el mismo nombre.
Una vez producido el cambio de nivel en uno o varios de los pines habilitados para interrupción, se activará el flag respectivo PCIF (Pin Change Interrupt Flag) del registro PCIFR y luego se llamará a la función de interrupción ISR. Así como hay un bit enable para cada puerto, en este nivel también hay un bit de flag correspondiente. Es de prever que el siguiente esquema pertenece a los AVR de 4 puertos. Allí, por ejemplo, si un pin de PORTB cambia de nivel, entonces se activará el flag PCIF1. PCIFR------------PCIF3PCIF2PCIF1PCIF0
Por supuesto, en los AVR de 3 puertos hay variación en el mapa del registro PCIFR (Pin Change Interrupt Flag Register). PCIFR---------------PCIF2PCIF1PCIF0
Como de costumbre, el flag PCIF será limpiado por el hardware al ejecutarse el gestor de interrupción ISR. Sin embargo, como este flag puede activarse sin necesidad de que esté seteado el bit enable general I (del registro SREG), a veces se tendrá que limpiar por software. En ese caso se limpia escribiendo un 1. Para evitar llegar a esta situación es recomendable habilitar la Interrupción de Cambio de Pin después de realizar en los puertos todas las operaciones necesarias que pudieran ocasionar cambios de nivel en sus pines, por ejemplo, activar las pull-ups. Finalmente, debemos tener en cuenta que hay un Vector de Interrupción [de Cambio de Pin] por cada puerto de AVR, llamados PCINT0_vect, PCINT1_vect, PCINT2_vect y PCINT3_vect. La activación del flag PCIFx conducirá a la ejecución de la función ISR identificada por el vector PCINTx_vect.
Práctica: Interrupciones de Cambio de Pin En el programa el AVR permanece en estado sleep (Power-down) y despierta cada vez que se presionen los pulsadores conectados a los pines PD5, PD6 y PD7. (Puede haber más o menos pulsadores y se pueden elegir cualesquiera ortos) Cada pulsador hará conmutar un LED conectado al puerto B. Los LEDs deben conmutar solo al presionar los pulsadores y no al soltarlos.
Circuito de la práctica. El código fuente /****************************************************************************** * FileName: main.c * Purpose: Uso de las Interrupciones de Cambio de Pin, PCINTx * Processor: ATmega AVR * Compiler: IAR-C y AVR-GCC (WinAVR) * Author: Shawn Johnson. http://www.cursomicros.com. * * Copyright (C) 2008 - 2012 Shawn Johnson. All rights reserved. * * License: Se permiten el uso y la redistribución de este código con * modificaciones o sin ellas, siempre que se mantengan esta * licencia y las notas de autor y copyright de arriba. *****************************************************************************/ #include "avr_compiler.h" //**************************************************************************** // Interrupt Service Routine // Esta función se ejecuta cuando se detectan cambios de nivel (flancos de // subida o de bajada) en los pines PD5, PD6 o PD7. //**************************************************************************** ISR (PCINT3_vect) { char dato = ~(PIND|0x1F); // Enmascarar bits PD5, PD6 y PD7 switch (dato) { case (1<<5): PINC = 0x01; break; // Cambio de pin PD5 case (1<<6): PINC = 0x02; break; // Cambio de pin PD6 case (1<<7): PINC = 0x04; break; // Cambio de pin PD7 default: nop(); // Cambio de más de un pin } delay_us(40000); // 40ms para pasar los rebotes
} //**************************************************************************** // Función principal //**************************************************************************** int main(void) { DDRC = 0x07; // Pines PC0, PC1 y PC2 para salida (LEDs) /* Habilitar pull-up de pines PD5, PD6 y PD7 (pulsadores) */ PORTD = (1<<5)|(1<<6)|(1<<7); /* Habilitar interrupciones PCINT de PORTD en los pines PD5, PD6 y PD7 */ PCICR = (1<
// Habilitación general de interrupciones
while(1) // Bucle infinito { /* Entrar en modo sleep (Power-Down mode) */ SMCR = (1<
Práctica: Control de Teclado por Interrupciones Alguna vez leí en uno de los documentos de Microchip que la interrupción de Cambio de PORTB de sus PICmicros fue pensada para controlar los pequeños teclados matriciales. En ese entonces los ATmega aún no tenían una característica similar. Pero como sabemos ahora, los ATmega como los que estamos estudiando llegaron más lejos y nos permiten manejar no solo uno sino varios teclados matriciales con el mínimo hardware, en estado sleep y desde cualquier puerto del microcontrolador. En este programa el AVR debe permanecer durmiendo (modo Power-down) y despertar solo cuando se pulsa una tecla para leerla y mostrarla en el terminal serial.
Circuito de la práctica. El código fuente /****************************************************************************** * FileName: main.c * Purpose: Control de Teclado mediante Interrupciones PCINTx * Processor: ATmega164P * Compiler: IAR-C y AVR-GCC (WinAVR) * Author: Shawn Johnson. http://www.cursomicros.com. * * Copyright (C) 2008 - 2012 Shawn Johnson. All rights reserved. * * License: Se permiten el uso y la redistribución de este código con * modificaciones o sin ellas, siempre que se mantengan esta * licencia y las notas de autor y copyright de arriba. *****************************************************************************/ #include "avr_compiler.h" #include "usart.h"
#include "keypad.h" void SetupInt(void); //**************************************************************************** // Interrupt Service Routine // Esta función se ejecuta cuando se detectan cambios de nivel (flancos de // subida o de bajada) en los pines PB4...PB7. //**************************************************************************** ISR (PCINT1_vect) { char dato = keypad_read(); // Leer teclado if(dato) // Si fue tecla válida { /* Esperar a que haya espacio en el buffer de transmisión */ while ((UCSR0A & (1<
// Habilitación general de interrupciones
while(1) // Bucle infinito { /* Entrar en modo sleep (Power-Down mode) */ SMCR = (1<
Según el circuito y la librería del teclado, cuando no hay teclas pulsadas las líneas Col (nibble alto de PORTB) se leen como „1‟ lógico (gracias a las pull-ups), y así deberían permanecer mientras el AVR está “soñando”. Por tanto, para que haya un cambio de nivel al pulsar una tecla, las líneas de Row (nibble bajo de PORTB) deberían sacar „0‟ lógico. De esto se encarga la función SetupInt. //**************************************************************************** // Prepara el puerto B para que detecte un cambio de tensión producido al // presionar una tecla. //**************************************************************************** void SetupInt(void) { /* Configurar PORTB: Nibble de Rows salida, Nibble de Rows bajo * nible alto entrada y habilitar pull-ups de nibble alto */ PORTB = 0xF0; DDRB = 0x0F; }
El USART y la Interface RS232
Introducción La interface con un ordenador se puede realizar por cualquiera de sus puertos externos más conocidos: serie, paralelo o el USB. El paralelo casi ni se encuentra en los ordenadores de hoy y de momento el puerto USB nos queda fuera de alcance por la complejidad del desarrollo del firmware (programa del microcontrolador). Así nos quedamos con el puerto serie. Aprenderemos a volcar datos desde nuestro microcontrolador a la pantalla del ordenador (data logging), así como a enviar datos mediante el teclado del PC hacia el microcontrolador.
El Estándar RS-232 Toda comunicación elaborada entre dos dispositivos requiere conocer el protocolo que la gobierna a nivel hardware y software. Para el puerto serie se trata del Estándar RS-232, o más bien EIA/TIA-232 por las siglas de Electronics Industry Association y Telecommunications Industry Association, sus desarrolladores. El RS-232 fue originariamente pensado para regir las comunicaciones entre ordenadores y equipos de módem de la época (hace más de 40 años). Con el tiempo han surgido otras versiones como RS-232-C, RS-232-D, RS-232-E, etc., una más reciente que la otra, pero con variaciones inapreciables por ser uno de los estándares menos estrictos. Después de todo, es solo un Estándar Recomendado o “Recommended Standard”; de ahí la RS. En la literatura técnica se acostumbra mucho utilizar los términos DTE y DCE para referir a los dispositivos que se comunican según el Estándar RS-232. DTE (Data Terminal Equipment) suele representar al ordenador y DCE (Data Circuit-terminating Equipment) designa a cualquier dispositivo conectado al ordenador (un módem se sobrentendía antes). Sin embargo, estos conceptos no quedan del todo claros en redes del tipo ordenador-ordenador o microcontrolador-microcontrolador usando el puerto serie. Así que por comodidad en adelante hablaremos de
ordenador y módem, viendo como módem ¾hasta donde quepa¾ a cualquier dispositivo conectable al puerto serie (el circuito de nuestro microcontrolador). Ahora pasemos a describir los principales aspectos que nos “recomienda” el estándar.
Voltajes de los Niveles Lógicos RS-232 En las comunicaciones seriales RS-232 los valores para representar los 1‟s y 0‟s lógicos son muy diferentes de los que estamos acostumbrados a usar en el mundo TTL. Allí no existen los 5V (para el 1) y 0V (para el 0). Para entenderlo más fácilmente veamos la siguiente figura, donde se compara la forma de onda de una señal RS-232 con la forma de onda de una señal digital convencional.
Niveles de tensión para los 1s y 0s lógicos. Puedes notar la enorme diferencia: los 1 lógicos se representan con voltajes negativos y los 0 lógicos, por voltajes positivos; además del amplio rango de los voltajes.
Un 1 lógico se expresa por una tensión de –5V a –15V. Este estado se llama spacing. Un 0 lógico se da cuando la tensión en cualquiera de las líneas es de +5V hasta +15V. Este estado se conoce como marking
Formato de Transferencia de Datos Como en toda comunicación serial, los datos viajan en grupos de bits. En este caso cada grupo o carácter consta de un bit Start, los bits de Datos (8 por lo general), un bit de Paridad (opcional) y finaliza con uno o dos bits de Stop.
Formato de un byte de dato en el Estándar RS-232.
Bit Start. Es la transición de 1 a 0 e indica el inicio de una transferencia. En la lógica RS-232 podría significar una transición de -15V a +15V y en lógica TTL es una transición de 5V a 0V. Bits de Datos. Forman los datos en sí que se desean transmitir. Cada dato puede ser de 5, 6, 7 u 8 bits. Por supuesto, siempre preferimos trabajar con 8 bits (1 byte). El primer bit a transmitir es el menos significativo o LSbit (Least Significant Bit). Bit de Paridad. Este bit es opcional y se puede enviar después de los bits de datos. Sirve para ayudar a detectar posibles errores en las transferencias de datos. Es muy raramente usado, primero, porque es poco efectivo (solo podría detectar errores, no corregirlos) y, segundo, porque hay mejores formas de tratamiento de errores. Bits Stop. Los bits de Stop son estados de 1 lógico. El Estándar dice que puede haber 1, 1.5 ó 2 bits de Stop al final de los datos (o del bit de paridad si lo hubiera).
Velocidad de Transmisión (Baud Rate) El Baud Rate es el número de bits que se transmiten por segundo. Debido a que estamos hablando de un tipo de transmisión asíncrona, no existe una señal de reloj que sincronice los bits de datos. Para que los dispositivos transmisor y receptor se entiendan correctamente también es necesario que operen con el mismo baud rate. Los valores más comunes que fija el Estándar RS232 son: 1200, 2400, 4800, 9600, 19200, 38400, 56000, 57600, 115200, 128000, 256000. Aunque las versiones más recientes del Estándar ponen un límite de 20 kbits, es común emplear los valores altos como 115200 (siempre que sea posible). Sabemos que no es lo mismo la interface entre un ordenador y un microcontrolador usando un cable de 2 m de longitud que conectarlo a un PLC a 8 m de distancia: la longitud del cable y la interferencia presente en el ambiente son factores a considerar a la hora de escoger el baud rate.
Señales del Puerto Serie Internamente el puerto serial de un ordenador es controlado por un circuito integrado (por ejemplo el 16750, de 40 pines). De esas líneas solo 9 salen al exterior y desembocan en un conector DB9 macho (el que nosotros vemos y donde conectábamos nuestro programador serial). Raras veces se ve que salen más líneas para llegar a un conector DB25. El uso de las 9 señales tiene más sentido cuando se trabaja con un módem. Por eso vamos a seguir hablando de módem, pese a que bien puede ser reemplazado por otro dispositivo.
Pines del conector DB9 (macho) del puerto serie. En la figura mostrada las direcciones de las flechas señalan si los pines son de entrada o de salida. Del mismo modo, los colores ayudan a asociar los pines con funciones análogas o complementarias, así:
TD y RD se encargan de transmitir y recibir los datos, respectivamente. RTS y CTS sirven para controlar el Control del Flujo de Datos (Handshaking) hardware. DTR, DSR y DCD intervienen en el establecimiento de la comunicación. Además de ellos, están la infaltable tierra (SG) y RI, usada exclusivamente en conexiones con un módem.
Ahora bien, cuando vamos a conectar el ordenador a un microcontrolador nuestro interés se puede reducir a tres líneas: TD, RD y SG. Las demás: o pueden ignorarse, o pueden conectarse al mismo puerto serie artificiosamente para evitar problemas de comunicación, o pueden usarse para implementar un Control del Flujo de Datos (Handshaking) hardware, con el microcontrolador emulando algunas funciones de módem. En cualquiera de los tres casos, eso dependerá del software de ordenador usado para controlar el puerto serie. En el conector hembra la posición de los pines se puede ver diferente a fin de establecer una conexión cruzada, es decir, para que el TD de un dispositivo se una con el RD del otro y viceversa. Lo mismo debe pasar con los pares RTS-CTS y DTR-DSR.
Control del Flujo de Datos (Handshaking) Generalmente el ordenador superará a su interlocutor (módem u otro) tanto en velocidad de transferencia como en los buffers de recepción de datos. Para que el módem no empiece a perder los datos llegados el Estándar contempla mecanismos de control de flujo de datos ya sea vía hardware o software. El control de flujo de datos por software se identifica por el uso de los caracteres Xon (ascii 0x11) y Xoff (ascii 0x13). El diálogo es así: cuando, por alguna razón, el módem ya no desea recibir más datos del ordenador entonces le envía el carácter Xoff diciéndole que suspenda la transmisión al menos temporalmente. Cuando el módem esté nuevamente dispuesto a aceptar más datos le enviará el carácter Xon y el ordenador reanudará la transferencia. Lo bueno del método es que el hardware requerido es el mínimo (ver la siguiente figura) y lo malo es que ambos dispositivos deben soportar las transferencias full duplex que exige el estándar RS232 para este caso.
Conexión básica para el Handshaking software. El control de flujo por hardware hace participar activamente a las líneas RTS y CTS. En un tipo de comunicación simplex el protocolo es éste: cuando el ordenador quiere enviar datos al módem pone un 1 en RTS. Si el módem está dispuesto a recibir esos datos, le responderá con un 1 por la línea CTS y el ordenador empezará a transmitir los datos; de otro modo, le responderá con un 0 y el ordenador tendrá que posponer el envío de datos. Cuando la comunicación es half duplex o full duplex el protocolo varía, pero eso ya no lo tocaremos aquí para no seguir enredándolo. Solo vemos lo suficiente del estándar, en este caso para entender las conexiones alternativas entre un ordenador y un microcontrolador que se usan en algunas ocasiones y que veremos en la siguiente sección.
Cableado para el handshaking hardware entre dos dispositivos. En el diagrama se han sumado las líneas DTR, DSR y DCD (de color marrón), por las que los dispositivos se informan el uno al otro si están listos o no para iniciar la comunicación.
Interface Serial Microcontrolador-Ordenador Como es de esperar, el enfoque se divide en dos partes:
Requerimientos Hardware
Nos vamos a enfocar en dos aspectos. Primero veamos el tema del transceiver. Dado que los niveles de tensión en el Estándar RS-232 (de –12V, 0V y +12V en el ordenador) no son compatibles con los niveles habituales de los microcontroladores (de 0 y 5V), se requiere de un transceiver que convierta estas tensiones de unos niveles a otros y viceversa. Sin duda, el MAX232 es el más famoso de todos. Como se ve en su esquema, mostrado abajo, el MAX232 puede trabajar con una fuente de alimentación de 5V y provee dos canales de transmisión y dos de recepción, aunque solo se suele usar un par. A su gran tamaño se suma como desventaja el uso de condensadores externos, para “bombear” la carga necesaria en los circuitos doblador e inversor de voltaje.
Interface entre un microcontrolador y un computador mediante el transceiver MAX232. El mismo fabricante del MAX232, Dallas Semiconductors, ofrece sus versiones mejoradas como el MAX203, que no requiere de capacitores externos, o el MAX202, que brinda protección contra cargas electrostáticas. Mejor aun para pequeños circuitos sería el DS275 (de 8 pines), el cual tampoco requiere de capacitores externos y cuenta con el par justo de drivers de transmisión y recepción de datos. Su principal inconveniente es que está diseñado para operar solo en transferencias half duplex. Para conocer más del funcionamiento interno de los transceivers es recomendable que consultes sus respectivos datasheets.
Interface entre un microcontrolador y un computador mediante el transceiver DS275. El segundo aspecto hardware que interesa es el relacionado con el Control del Flujo de Datos (Handshaking): en los dos esquemas presentados anteriormente las retro-conexiones en el conector DB9 (de color violeta) son opcionales. Solo harán falta cuando el programa terminal del ordenador esté configurado para utilizar los pines indicados, así: RTS (7) se conecta a CTS (8) para que siempre que el ordenador desee enviar datos al microcontrolador, se responda a sí mismo con un “permiso concedido”. Análogamente, DTR (4) se une a DSR (6) para que cuando el ordenador informe un “estoy listo para la comunicación”, su eco (haciéndose pasar por el microcontrolador) le responda con un “yo también lo estoy”. A veces DTR también se dirige a DCD (1).
Requerimientos Software Por un lado necesitamos unas rutinas para el microcontrolador que gestionen las funciones del Estándar RS232. Éstas pueden implementarse tranquilamente a nivel software debido a su simplicidad o mediante el módulo USART, el cual por supuesto ofrecerá mucha más eficiencia y flexibilidad. Por otro lado, necesitaremos un programa de ordenador que se encargue de controlar su puerto serie. A su vez, este programa puede ser uno desarrollado por nosotros mismos, que nos permitiría tener el control total del puerto serie y podríamos transmitir y recibir todo tipo de datos (binarios o de texto). También podríamos implementar técnicas alternativas de control de flujo de datos (aparte de los descritos arriba), o sofisticados mecanismos para el control de errores en la transferencias de datos. Como ves, se ve muy atractivo, pero también requiere de conocimientos a mediano nivel sobre programación en lenguajes como Visual C++, Delphi o Visual Basic. Como alternativa práctica, podemos usar softwares como el Hyperterminal de Windows, Serial Port Monitor, Putty o Tera Term. Estos son programas de tipo consola que nos permiten visualizar los datos que se transfieren hacia/desde el puerto serie. Por no ofrecer tanta flexibilidad nos limitaremos a trabajar con datos de texto. Conforme vamos escribiendo los caracteres en la consola, se irán enviando hacia nuestro microcontrolador. Así mismo, los caracteres enviados desde el microcontrolador se irán mostrando en la consola, todo en tiempo real.
Interface del programa Tera Term
Uso del Programa Tera Term Ya tenemos todo listo para el microcontrolador. Ahora nos falta ejecutar algún programa terminal en nuestro ordenador para empezar el intercambio de datos. De los muchos que hay vamos a elegir el Tera Term, que es lo mejor que he podido encontrar y no lo digo precisamente porque sea de licencia GPL. Lo puedes bajar libremente desde su web http://ttssh2.sourceforge.jp, o haciendo clic aquí. “Tera Term es un emulador de terminal (programa de comunicaciones) que soporta:
Conexiones con el puerto serie. Conexiones TCP/IP (telnet, SSH1, SSH2). Comunicaciones IPv6 Emulación de VT100 y de VT200/300. Emulación de TEK4010. Protocolos de Transferencia de Archivos (Kermit, XMODEM, YMODEM, ZMODEM, B-PLUS y Quick-VAN). Scripts usando el ‘Lenguaje Tera Term’. Sets de caracteres Japonés, Inglés, Ruso y Coreano. Codificación de caracteres UTF-8.”
Bien, una vez descargado, los instalas aceptando lo que se te pida hasta llegar a
Si no los vas a utilizar o si no tienes idea de lo que significan, te recomiendo desmarcar las casillas de TTSSH, CygTerm+, LogMeTT, TTEdit y TTProxy. De lo contario, la instalación te pedirá aceptar los términos y la configuración de cada aplicación por separado. Suponiendo que seguiste mi recomendación, la instalación terminará enseguida y solo se creará un icono de acceso en el escritorio. Cada vez que iniciemos Tera Term se nos presentará la siguiente ventana, donde debemos escoger la opción Serial y en Port debemos seleccionar el puerto serie COM a usar. Normalmente los ordenadores actuales solo tienen un puerto COM disponible, el COM1. Después de hacer clic en OK se abrirá el puerto serial. Tera Term es muy potente y será capaz de quitarle el control del puerto a alguna otra aplicación que lo esté utilizando.
Y así de rápido estaremos al frente de la siguiente ventana, que ya podemos utilizar para nuestras comunicaciones seriales, ya que su configuración por defecto suele ser la más habitual. La barra de título indica que el baud rate usado es de 9600 y que se tiene seleccionado el puerto COM1.
De vez en cuando será necesario cambiar la configuración de comunicación que usa Tera Term por defecto. Para ello debemos ir al menú Setup Serial Port… El parámetro Transmit delay es el retardo que habrá entre cada uno de los datos que se envíen y/o entre cada línea de caracteres. Quizá interese incrementar este retardo cuando el microcontrolador no etnga la capacidad de recibir los datos con la prestancia necesaria. En cuando a los otros parámetros, ya los discutimos de sobra en el Estándar RS232.
Otra ventana donde encontrar algunas opciones útiles está en menú Setup Terminal… New-line establece si el cursor pasará a la siguiente línea con los caracteres CR (Carriage Return = 0x0D = „\r‟) y/o LF (LineFeed =
Newline= 0x0A = „\n‟). Por lo general estos caracteres se manejan a nivel firmware (desde el programa del microcontrolador), del mismo modo el eco lo hace e microcontrolador y raras veces será recomendable marcar la casilla de Local echo.
Ahora vamos a modificar el aspecto del programa. Puede que sea un tema superfluo pero al menos para mí esta apariencia vale mucho. Primero cambiaremos el tipo de fuente yendo al menú Setup Font… Y bueno, qué te puedo decir, escoge la fuente de tu preferencia.
Para quienes deseen que la consola luzca como el terminal simulado de Proteus pueden ir al menú Setup Window. Allí escogemos la forma del cursor (Cursor Shape) como Horizontal line. Luego en el marco Color empezamos por cambiar el color de fondo (Background) a negro y después el color de fuente (Text) a verde como se indica en la figura.
Ya te habrás dado cuenta de que la ventana del terminal se limpia cada vez que cambias su tamaño. Para evitar que lo haga debemos ir al menú Setup Aditional settings… y desmarcar la opción Clear display when window resized, como se ve abajo.
Para limpiar la ventana podemos usar las opciones Clear screen y Clear buffer del menú Edit. La primera opción simplemente recorre el contenido de la ventana, por eso es preferible usar Clear buffer, que resetea todo el contenido de la ventana. Las otras opciones de este menú hablan por sí solas.
Para terminar configuraremos el uso del teclado numérico. Esto no es un aspecto decorativo, es muy importante y lo deje para el final solo por ser algo engorroso. Por defecto, Tera Term emula el teclado VT100, por lo que algunas teclas como „+‟ o „-‟ no funcionarán como en un teclado convencional. Bueno, para ir directo al grano, seguimos el directorio de instalación de Tera Term, que en Windows 7 suele ser C:\Program Files (x86)\teraterm/ y utilizamos un editor de texto como el bloc de notas o Notepad++ para abrir el archivo KEYBOARD.CNF. Este archivo tiene varias secciones; nosotros nos ubicamos en [VT numeric keypad], que empieza en la línea 29 como se ve abajo.
Las líneas precedidas de punto y coma (;) son comentarios y no cuentan. Son los números mostrados en naranja los que debemos editar: debemos cambiarlos todos por la palabra off, desde Num0 hasta PF4, 18 en total, y debe quedar como en la siguiente figura.
Ahora guarda el archivo con los cambios realizados y reinicia el programa Tera Term. O también puedes ir al menú Setup Load key map… y recargar manualmente el archivo KEYBOARD.CNF. Finalmente, puesto que usaremos este programa con tanta frecuencia y no querremos estar configurándolo cada vez que lo ejecutemos, debemos guardar la configuración realizada yendo al menú Setup Save setup… en la ventana presentada simplemente hacemos clic en Guardar y con eso bastará para que Tera Term se abra siempre con la configuración actual.
El USART, USART0 y USART1 de los AVR
USART es la sigla de Universal Synchronous Asynchronous Receiver Transmitter. Es el periférico que incorporan muchos microcontroladores para comunicarse con dispositivos que soportan el estándar RS-232. De su nombre se deprende que puede trabajar en modo Síncrono o Asíncrono. En esta presentación nos enfocaremos exclusivamente al modo asíncrono. Algunos AVR (como los viejos ATmega32, ATmega8535, etc.) poseen un solo módulo USART llamado simplemente USART. Los AVR mejorados como los que utilizamos en cursomicros tienen uno o dos módulos, llamados USART0 y USART1, con características de control idénticas. Por ejemplo, los ATmega de la serie 8xx tienen USART0 y los ATmega de la familia 4xx tienen adicionalmente el USART1. El USART0 y el USART1 son gemelos. Entre ellos y los viejos USART de los AVR hay una mínima diferencia que muchas veces se pasará por alto sin notarlo. De hecho, aparte de los nombres de los registros de relacionados, el control del USART es compatible en todos los AVR que lo tienen; incluso hay un gran parecido con los de otros microcontroladores. Así que creo que valdrá la pena demorarnos un poco en esta teoría. Las características comunes del USART en los AVR son:
Asíncronamente pueden trabajar en modo full-duplex, esto es transmitir y recibir datos, al mismo tiempo inclusive. Pueden generar interrupciones al recibir datos o después de enviarlos. Puede operar en modo SPI maestro. Esta opción no está disponible en los viejos AVR. Operan en background (detrás del escenario) para que las transferencias de datos se lleven a cabo mientras el CPU realiza otras tareas. El baud rate es configurable por el usuario. Los datos pueden ser de 5, 6, 7, 8 ó 9 bits. Puede trabajar con uno o dos bits de Stop. Y además de aceptar el bit paridad puede computar a nivel hardware su valor para el dato actual.
Los Registros del USART Además de estos registros todavía faltan por citar los que controlan las interrupciones del USART. Aunque ya los conocemos bastante bien los veremos por separado. Los nombres de los registros y de sus bits varían de acuerdo con el número de USART. En toda mi exposición tomo como referencia el USART0 y por ello verás el número 0 acompañando a cada registro. Si utiliza el USART1 se debe cambiar el 0 por el 1 en cada registro y en cada bit; y si se utiliza el USART viejo simplemente se quita el 0. Por ejemplo, en el USART1 el registro UCSR0A se debe suprimir por UCSR1A. Lo mismo debe aplicarse a los nombres de cada bit.
UDR0. El USART tiene dos buffers para las transferencias de datos: un buffer de transmisión (donde se cargan los datos a trasmitir) y un buffer de recepción (donde se almacenan los datos recibidos). Mediante el registro UDR0 se accede a ambos buffers. Esto es, se accede al buffer de transmisión (en las operaciones de escritura) y al buffer de recepción (en las operaciones de lectura). UDR0 significa USART Data Register 0. Su nombre en los USART1 es UDR1 y en los USART viejos se llama simplemente UDR. UCSR0A, UCSR0B y UCSR0C. (USART Control and Status Register A, B y C). Son los Registros de Control y Estado del USART0. Creo que los nombres lo dicen todo. Para los que aún usan los viejos AVR, deben saber que los registros UCSRC y UBRRH comparten la misma locación en espacio de los registros de E/S. Ellos deben setear el bit 7 para escribir en el registro UCSRC y limpiarlo para escribir en UBRRH. Las operaciones de lectura son inusuales. UBRR0L y UBRR0H. Son los registros generadores de Baud Rate del USART0. Juntos forman un registro de 16 bits cuyo valor establece la velocidad de transferencia de datos.
Todos estos registros son de 8 bits. UDR0 y el último par no tienen formatos preestablecidos y podrían aceptar cualesquiera valores. Los registros de control y estado tienen los siguientes mapas de bits: UCSR0ARXC0 TXC0 UDRE0 UCSR0BRXCIE0 TXCIE0 UDRIE0 UCSR0CUMSEL01 UMSEL00 UPM01
FE0 RXEN0 UPM00
DOR0 TXEN0 USBS0
UPE0 UCSZ02 UCSZ01
U2X0 RXB80 UCSZ00
MPCM0 TXB80 UCPOL0
En lo sucesivo iremos describiendo las funciones de estos dos registros y de cada uno de sus bits. Algunos bits están relacionados con la operación del USART en modo síncrono, tema que de momento no nos compete, y serán ignorados.
Inicialización del USART Lo primero que debemos hacer con el USART es configurar su operación: esto es, establecer el modo de operación, fijar el formato de los datos, poner la velocidad de las transferencias y habilitar los módulos Receptor y/o Transmisor. Los bits de los registros de Control y Estado relacionados con la configuración de la librería [usart.h y usart.c] usada en cursomicros.com son los siguientes. Los bits no mencionados los trataremos de cerca al estudiar los modos de operación Síncrono y SPI Master. MPCM0. Recordemos que inicialmente el estándar RS232 permite comunicaciones solo entre dos dispositivos (DTE y DCE). Actualmente los USART también soportan comunicaciones con otros protocolos como del RS485, donde se pueden engarzar al bus varios dispositivos en relaciones maestro-esclavo y con direcciones que los identifican (parecido al bus I2C). Para que el USART del AVR pueda integrarse a esas redes debemos setear el bit MPCM0 (Multiprocessor Communication Mode). Para comunicaciones RS232 este bit se debe mantener en cero. TXEN0 y RXEN0. Son los bits para habilitar los módulos Transmisor y Receptor del USART. No es necesario habilitar los dos módulos al mismo tiempo. Al habilitar un módulo (seteando el bit TXEN0 o RXEN0), éste asumirá el control del pin respectivo (TXD0 para el transmisor y RXD0 para el receptor) y por tanto dichos pines dejarán de funcionar como entrada y salida generales, sin importar el valor de los bits correspondientes en los registros DDR, PORT y PIN. UMSEL01 y UMSEL00. Estos bits establecen uno de los tres modos en que puede trabajar el USART0: Modo Asíncrono, modo Síncrono y modo SPI Maestro. La configuración por defecto es Asíncrono, con ambos bits iguales a cero, así que de momento no tendremos que tocarlos. USBS0. Si este bit vale 0, el módulo transmisor enviará un bit Stop al final de cada dato. Si vale 1, enviará 2 bits Stop. El módulo receptor solo evalúa el primer bit Stop recibido, así que no le interesa esta configuración. Tampoco tocaremos este bit puesto que trabajaremos con un solo bit Stop porque es el valor preestablecido en todos sistemas RS232, y así nos ahorramos settings adicionales ;). UCSZ02, UCSZ01 y UCSZ00. Estos bits establecen el tamaño de los datos que se utilizarán en las transferencias, los cuales pueden ser desde 5 hasta 9 bits. El formato seleccionado será empleado por ambos módulos, transmisor y receptor. Si los datos son de 8 bits se trabajará con el valor completo del registro UDR0; si los datos son de menos bits se obviarán los bits de mayor peso del registro UDR0; y si los datos son de 9 bits, los 8 bits de UDR0 se complementarán con el bit TXB80 (para las transmisiones) y RXB80 (para las recepciones). Por defecto todos estos bits inician a cero, así que observando la siguiente concluimos en que deberemos cambiarlos a 011.
UCSZ02UCSZ01UCSZ00Tamaño del Carácter 0 0 0 5 bits 0 0 1 6 bits 0 1 0 7 bits 0 1 1 8 bits 1 0 0 Reservado 1 0 1 Reservado 1 1 0 Reservado 1 1 1 9 bits U2X0. Como habíamos estudiado, el baud rate es la velocidad de transferencias de datos, se mide en bits/segundo y hay valores estándar que debemos respetar (9600, 19200, 115200, etc.). Seteando el bit U2X0 se divide el prescaler generador de baud rate de modo que la velocidad de transferencia de datos se multiplique por dos. Este bit solo tiene efecto en el modo Asíncrono y dependiendo de su valor se disponen de dos fórmulas para calcular el valor del baud rate final. F_CPU es la frecuencia del procesador y recuerda que es una constante definida en el archivo avr_compiler.h. U2X0 = 1 (Velocidad Doble)
U2X0 = 0 (Velocidad Normal)
UBRR0 está conformado por la unión de dos registros de E/S: UBRR0H y UBRR0L. Con su capacidad de 16 bits y la fórmula de U2X0 = 1 se pueden obtener los suficientes valores de baud rates como para hacernos olvidar de la fórmula con U2X0 = 0. De hecho, analizando las fórmulas se deduce que cualquier valor de baud con U2X = 0 también se puede conseguir con la fórmula de U2X0 = 1, aunque a veces esto devendrá en una menor performance en el muestreo de la señal de recepción. En la práctica no interesa tanto el hecho de que U2X0 = 1 “incremente” la velocidad de las transferencias, sino que se pueden obtener baud rates más precisos. En consecuencia, la mejor decisión en la mayoría de los casos será escoger la primera fórmula, con U2X = 1. De allí debemos despejar UBRR0 para calcular su valor. Esto nos da siguiente expresión, que escrita en forma lineal sería UBRR0 = F_CPU/(8*BAUD) – 1. El valor generado para UBRR0 raras veces será exacto y en tales casos se deberá escoger el más cercano y recalcular el valor del baud rate. Debemos tener cuidado con los resultados obtenidos puesto que hay valores impuestos por el estándar RS232 y que debemos respetar. En todo caso, puedes revisar las tablas de baud rate presentes en los datasheets. Con todo lo expuesto ya podemos implementar la siguiente función de inicialización del USART0. Es irónico que tanta teoría se condense en tan poco código, pero es más gracioso saber que la configuración establecida se puede resumir con la notación BAUD 8N1, que leído en orden significa Baud rate = BAUD, formato de datos = 8 bits, No (sin) bit de paridad y 1 bit de Stop. Deberemos recordar esos parámetros a la hora de configurar el software terminal del lado del ordenador. //**************************************************************************** // Inicializa el USART0. //**************************************************************************** void usart_init(void)
{ /* Configurar baud rate */ UCSR0A |= (1<
*/
#if defined( __GNUC__ ) /* Asociar las funciones 'putchar' y 'getchar' con las funciones de entrada * y salida (como printf, scanf, etc.) de la librería 'stdio' de AVR-GCC */ fdevopen((int (*)(char, FILE*))putchar, (int (*)(FILE*))getchar); #endif }
Una aclaración para quienes usen los viejos AVR: en dichos AVR los registros UBRRH y UBRRL se encuentran bastante lejos uno del otro en el espacio de los registros de E/S. Como consecuencia la sentencia de asignación UBRR = F_CPU/(8*USART_BAUD)-1; no será válida. Tendrán que escribirlos por separado
Transmisión de Datos El dato que el USART0 transmitirá debe ser previamente cargado en el registro UDR0. Con ello el dato pasará al Registro de Desplazamiento de Transmisión si es que está vacío. Luego el dato saldrá serialmente bit a bit por el pin TXD0. Pero si el Registro de deslazamiento se encuentra transmitiendo un dato previo, el nuevo dato permanecerá en UDR0 hasta que el registro de desplazamiento quede disponible. En ese lapso de tiempo el registro UDR0 no podrá aceptar un nuevo dato.
Módulo de Transmisión del USART0. Mientras el registro UDR0 contenga algún dato, el bit UDRE0 (del registro UCSR0A) valdrá 0. Cuando UDR0 esté nuevamente libre (cuando haya descargado su contenido en el registro de desplazamiento), el flag UDRE0 se seteará automáticamente por hardware. Por tanto, será necesario comprobar que el bit UDRE0 valga 1 antes de intentar escribir un nuevo dato en UDR0. La activación del flag UDRE0 puede generar una interrupción si es que está habilitada. Detallaremos las interrupciones del USART en otra sección más adelante.
Una conclusión de lo descrito es que es posible escribir en UDR0 hasta dos datos sin pausa intermedia, el primero irá al registro de desplazamiento y el segundo se quedará en UDR0. Así podemos decir que el USART0 tiene un buffer FIFO de transmisión de dos niveles. Con lo expuesto ya se puede codificar la siguiente función putchar. El encabezado de esta función está de acuerdo con su definición en la librería stdio.h del compilador C. Allí se indica que putchar debe tener un parámetro de entrada y uno de salida, ambos de tipo int, aunque no en la práctica no parezca que sea necesario. //**************************************************************************** // Transmite el byte bajo de 'dato' por el USART //**************************************************************************** int putchar(int dato) { /* Esperar a que haya espacio en el buffer de transmisión */ while ((UCSR0A & (1<
Existe un flag adicional llamado TXDC (en el registro UCSR0A) que se setea cuando se haya terminado de transmitir el dato del registro de desplazamiento y al mismo tiempo no exista otro dato presente en el registro UDR0. Este flag también tiene la capacidad de disparar una interrupción ante dicho evento. En ese caso el flag TXDC se limpiará automáticamente al ejecutarse la función ISR; pero siendo de lectura/escritura también se puede limpiar por software escribiéndole uno sobre él.
Recepción de Datos Los datos seriales que llegan ingresan bit a bit por el pin RXD0 y se van depositando en el Registro de Desplazamiento de Recepción. Cuando el dato esté completo (cuando llegue su bit Stop) pasará paralelamente al registro UDR0. Luego podremos leer el dato del registro UDR0. Pero si el registro UDR0 está ocupado con datos previos que no han sido leídos por el procesador, el nuevo dato permanecerá en el registro de desplazamiento hasta que haya espacio en UDR0.
Módulo de Recepción del USART0.
Apenas haya un dato en UDR0 se seteará el flag RXC0 (del registro UCSR0A). Así que para saber si hay un dato allí pendiente de ser leído debemos comprobar el bit RXC0. Esté flag es de solo lectura y se limpiará automáticamente cuando hayamos terminado de leer todos los datos del buffer UDR0. La activación del flag RXC0 también puede generar una interrupción si está previamente habilitada. No debemos confundir el UDR0 de recepción con el UDR0 de transmisión. Aunque tengan el mismo nombre, corresponden a dos registros diferentes. Es más, el UDR0 de recepción en realidad es un buffer de dos niveles (puede almacenar hasta dos datos). Ahora podemos decir que el USART0 tiene un buffer FIFO capaz de almacenar hasta tres datos al mismo tiempo: dos en el buffer UDR0 y uno en el registro de desplazamiento. Si en este momento se detectara la llegada de un nuevo dato por el pin RXD0, dicho dato se perdería y como señal se activaría el flag de desbordamiento DOR0 (Data OverRun). Como no querremos que ocurra dicho evento trágico será recomendable que leamos de inmediato cada nuevo que llegue al USART. Siguiendo esta recomendación la función de recepción tendrá el siguiente aspecto. Esta implementación también toma como referencia la definición de getchar presente en el archivo stdio.h del compilador C. //**************************************************************************** // Recibe un byte de dato del USART //**************************************************************************** int getchar(void) { /* Esperar a que haya al menos un dato en el buffer de recepción */ while ((UCSR0A & (1<
Registros del USART0 El Registro UCSR0A UCSR0ARXC0 TXC0 UDRE0 RXC0 USART Receive Complete
TXC0
FE0
DOR0
UPE0
U2X0
Este bit de flag vale uno cuando hay datos no leídos en el buffer de recepción y vale cero cuando el buffer de recepción está vacío (esto es, no contiene datos por leer). Si el módulo receptor está deshabilitado, el buffer de recepción será liberado y consecuentemente el bit RXC0 se pondrá a cero. E flag RXC0 se puede usar para generar una Interrupción de Recepción Completada (ver la descripción del bit RXCIE0). USART Transmit Complete Este bit de flag se pone a uno cuando un dato completo ha terminado de salir del Registro de Desplazamiento de Transmisión y no hay ningún dato presente en el registro UDR0. El bit TXC0 se limpia automáticamente cuando se ejecuta la función de interrupción, o se puede limpiar por software escribiendo uno sobre él. El flag TXC0 puede generar la Interrupción de Transmisión Completada (ver la descripción del bit TXCIE0).
MPCM0
UCSR0ARXC0 TXC0 UDRE0 UDRE0 USART Data Register Empty
FE0
DOR0
UPE0
U2X0
FE0
El flag UDRE0 indica si el buffer de transmisión (UDR0) está listo para recibir un nuevo dato. Si el bit UDRE0 vale uno, el buffer está vacío, y por tanto está listo para ser escrito. El flag UDRE0 puede generar una Interrupción de Registro de Dato Vacío (ver descripción del bit UDRIE0). El bit UDRE0 se pone a uno después de un reset para indicar que el Transmisor está listo. Frame Error
DOR0
Este bit se pone a uno si el siguiente carácter en el buffer de recepción tuvo un error de frame en la recepción del dato. Esto es, cuando el primer bit Stop del siguiente carácter en el buffer de recepción es cero. Este bit será válido hasta que se lea el buffer de recepción UDR0. El bit FE0 vale cero cuando el bit Stop del dato recibido es uno. Siempre que se escriba en el registro UCSR0A este bit se debe mantener en cero. Data OverRun
UPE0
Este bit se pone a uno cuando se detecta una condición de desbordamiento de dato. Ocurre un desbordamiento de dato (Data OverRun) cuando el buffer de recepción está lleno (con dos caracteres), contiene un nuevo carácter esperando en su Registro de Desplazamiento, y se detecta un nuevo bit Start. Este bit es válido hasta que se lea el buffer UDR0. Siempre que se escriba en el registro UCSR0A este bit se debe mantener en cero. USART Parity Error
U2X0
Este se pone a uno al detectarse un Error de Paridad en el buffer de recepción cuando están habilitados la recepción del bit de Paridad y su comprobación (UPM01 = 1). Este bit será válido hasta que se lea el buffer UDR0. Siempre que se escriba en el registro UCSR0A este bit se debe mantener en cero. Double the USART Transmission Speed
MPCM0
Este bit solo tiene efecto en el modo de operación asíncrono. Se debe escribir cero en este bit cuando se usa el modo de operación síncrono.
MPCM0
En las comunicaciones asíncronas, si se escribe uno en este bit se reducirá el divisor del generador de baud rate de 16 a 8, dando como resultado la multiplicación por dos de la velocidad de transferencia de datos. Multi-processor Communication Mode Este bit habilita el modo de Comunicación Multiprocesador. Cuando se escribe uno en el bit MPCM0, serán ignorados todos los frames recibidos por el USART que no contengan información de dirección. El módulo Transmisor no queda afectado por la configuración de los bits MPCM0.
El Registro UCSR0B UCSR0BRXCIE0 TXCIE0 UDRIE0 RXCIE0 RX Complete Interrupt Enable
RXEN0
TXEN0
UCSZ02
RXB80
Al escribir uno en este bit se habilita la Interrupción de Recepción Completada cada vez
TXB80
UCSR0BRXCIE0 TXCIE0 UDRIE0 RXEN0 TXEN0 UCSZ02 RXB80 TXB80 que se active el flag RXC0. Para que se genere la interrupción será necesario que también el bit enable general I de SREG valga uno. TXCIE0 TX Complete Interrupt Enable
UDRIE0
Al escribir uno en este bit se habilita la Interrupción de Transmisión Completada cada vez que se active el flag TXC0. Para que se genere la interrupción será necesario que también el bit enable general I de SREG valga uno. USART Data Register Empty Interrupt Enable Al escribir uno en este bit se habilita la interrupción al activarse el flag UDRE0. Se generará una Interrupción de Dato de Registro Vacío solo si valen uno el bit UDRIE0, el flag de Global de Interrupciones en el registro SREG y el bit UDRE0 en el registro SREG.
RXEN0
Receiver Enable
TXEN0
Al escribir uno en este bit se habilita el módulo Receptor del USART. El receptor tomará el control del pin RXD0. Al deshabilitar el receptor se liberará el buffer de recepción invalidando los flags FE0, DOR0 y UPE0. Transmitter Enable
UCSZ02
Al escribir uno en este bit se habilita el módulo Transmisor del USART. El transmisor tomará el control del pin TXD0. La des-habilitación del transmisor (escribiendo 0 en TXEN0) no se hará efectiva hasta que se completen las transmisiones en marcha y las pendientes, esto es, cuando el registro UDR0 y el registro de desplazamiento estén vacíos. Cuando se deshabilite el Transmisor el pin TXD0 quedará en libertad. Character Size
RXB80
El bit UCSZ02 combinado con los bits UCSZ01 y UCSZ00 del registro UCSRC establece el número de los bits de datos (tamaño del carácter) en los frames que usarán el Transmisor y el Receptor. Receive Data Bit 8
TXB80
RXB80 es el noveno bit del dato recibido cuando se trabaja con datos seriales de 9 bits. Se debe leer antes de leer los bits bajos del registro UDR0. Transmit Data Bit 8 TXB80 es el novena bit del dato a transmitir cuando se trabaja con datos de 9 bits. Se debe escribir antes de escribir los bits bajos del registro UDR0.
El Registro UCSR0C UCSR0C UMSEL01 UMSEL00 UPM01 UPM00 USBS0 UCSZ01 UCSZ00 UCPOL0 UMSEL01 USART Mode Select UMSEL00 Estos bits seleccionan el modo de operación del USART como se muestra en la siguiente tabla. UMSEL01 UMSEL00 Mode 0 0 USART Asíncrono
UCSR0C UMSEL01 UMSEL00 UPM01 UPM00 USBS0 UCSZ01 UCSZ00 UCPOL0 0 1 USART Síncrono 1 0 Reservado 1 1 Master SPI (MSPIM) UPM01: Parity Mode UPM00 Estos bits habilitan y configuran la generación y comprobación del bit de paridad. Si está habilitado, el Transmisor generará y enviará automáticamente el bit de paridad de cada dato. El Receptor calculará el bit de paridad de cada dato recibido y lo comparará con la configuración del bit UPM00. Si se detecta una discordancia, se seteará el flag UPE0 del registro UCSR0A.
USBS0
UPM01 UPM00 0 0 0 1 1 0 1 1 Stop Bit Select
Modo de Paridad Deshabilitado Reservado Habilitado, Paridad Par Habilitado, Paridad Impar
Este bit selecciona el número de bits Stop que utilizará el Transmisor. El Receptor “ignora” esta configuración.
UCSZ01: UCSZ00
USBS0 0 1 Character Size
Bits Stop 1 bit 2 bits
Estos bits se combinan con el bit UCSZ02 del registro UCSR0B para establecer el número de bits de los datos (tamaño de carácter) que utilizarán el Transmisor y el Receptor.
UCPOL0
UCSZ02 UCSZ01 UCSZ00 Tamaño del Carácter 0 0 0 5 bits 0 0 1 6 bits 0 1 0 7 bits 0 1 1 8 bits 1 0 0 Reservado 1 0 1 Reservado 1 1 0 Reservado 1 1 1 9 bits Clock Polarity Este bit solo se usa en el modo de operación Síncrono. En el modo Asíncrono debe permanecer en cero. El bit UCPOL0 establece la relación entre el cambio de los datos de salida y la señal de reloj XCK0, y la relación entre el muestreo de los datos de entrada y la señal de reloj XCK0. UCPOL0 0
El Cambio de Datos ocurre (A la salida del pin TxD0) En el flanco de Subida del pin
El Muestreo de Datos ocurre (A la entrada del pin RxD0) En el flanco de Bajada del pin
UCSR0C UMSEL01 UMSEL00 UPM01 UPM00 USBS0 UCSZ01 UCSZ00 XCK0 XCK0 En el flanco de Bajada del pin En el flanco de Subida del pin 1 XCK0 XCK0
UCPOL0
Los Registros UBRR0L y UBRR0H UBRR0H--UBRR0L Bit 7 Bit 15:12 Reserved
--Bit 6
--Bit 5
--Bit 4
Bit 11 Bit 3
Bit 10 Bit 9 Bit 8 Bit 2 Bit 1
Bit 0
Estos bits están reservados para usos futuros. Cuando se escriba en UBRR0H este bit debe permanecer en cero por compatibilidad con futuros dispositivos. Bit 11:0 UBRR11:0: USART Baud Rate Register Este es un registro de 12 bits que contiene el baud rate del USART. El registro UCRR0H contiene los 4 bits más significativos, y UBRR0L contiene los 8 bits menos significativos del baud rate del USART. Si se cambia el baud rate cuando haya transmisiones o recepciones de datos en curso, estas operaciones serán estropeadas. La escritura en UBRR0L actualizará de inmediato el prescaler del baud rate.
Tablas de Baud Rate Algunos compiladores como CodeVisionAVR pueden calcular automáticamente el valor de los registros UBRR para generar el baud rate que le indiquemos, pueden incluso avisar el error producido. En otros casos será recomendable ver por nosotros mismos si el error es aceptable para nuestras aplicaciones. Si es una cuestión crucial, también se puede optar por cambiar de XTAL por otro cuya frecuencia sea múltiplo del baud rate deseado; por ejemplo, para los baud rates múltiplos de 9600 (que son la gran mayoría) los XTALes de 3.6884 MHz, 11.0592 MHz ó 18.4320 MHz derivan en errores de 0.00%.
Práctica: Hello
El programa maneja el ingreso y salida de cadenas de texto.
Circuito de la práctica. El código fuente /****************************************************************************** * FileName: main.c * Purpose: Comunicación básica por USART0 * Processor: ATmel AVR con módulo TWI * Compiler: IAR-C y AVR-GCC (WinAVR) * Author: Shawn Johnson. http://www.cursomicros.com. * * Copyright (C) 2008 - 2012 Shawn Johnson. All rights reserved. * * License: Se permiten el uso y la redistribución de este código con * modificaciones o sin ellas, siempre que se mantengan esta * licencia y las notas de autor y copyright de arriba. *****************************************************************************/
#include "avr_compiler.h" #include "usart.h" int main(void) { char name[20]; usart_init();
// Inicializar USART0 @ 9600-8N1
puts("\n\r www.cursomicros.com "); puts("\n\r =================== \n\r"); puts("\n\r - ¿Cuál es tu nombre?... \n\r - "); scanf("%s", name); printf(" - Gusto en conocerte, %s", name); while(1); }
Para que el programa responda como se muestra en la siguiente imagen será necesario que configurar el uso del eco ya sea en el entorno de Tera Term, como se indicó en la sección Uso de Tera Term, o en el archivo usart.h, como se indicó en la sección Librerías para el USART. De lo contrario la función scanf no mostrará el texto que vayas ingresando por el teclado.
Las Interrupciones del USART
Seguiremos considerando que todo lo expuesto es igualmente válido para el USART1 e incluso para el USART de los viejos AVR. Con eso aclarado… El USART0 puede generar tres interrupciones, a citar:
Interrupción de Recepción Completada. Cuando se acaba de recibir un nuevo dato, esto es, cuando un dato recién llegado acaba de pasar del Registro de desplazamiento al buffer del registro UDR0 se activará el flag RXC0 el cual podrá disparar la interrupción, si es que está habilitada. Esta interrupción se habilita seteando el bit RXCIE0, aparte del bit enable general I, claro está. El flag RXC0 es algo especial porque es de solo lectura y no se limpia automáticamente al ejecutarse la función de interrupción ISR, sino únicamente cuando el buffer de recepción esté vacío. Esto significa que si la interrupción está habilitada se disparará sucesivamente mientras haya uno o más datos por leer.
UCSR0ARXC0 UCSR0BRXCIE0
TXC0 TXCIE0
UDRE0 UDRIE0
FE0 RXEN0
DOR0 TXEN0
UPE0 UCSZ02
U2X0 RXB80
MPCM0 TXB80
Interrupción de Registro de Datos Vacío. Se refiere al registro de datos UDR0 del módulo transmisor. Esta interrupción se habilita seteando el bit UDRIE0 (aparte de I), y se disparará cuando se active a uno el flag UDRE0. Este evento ocurre cuando el registro UDR0 está vacío, sea después de un reset o después de depositar su contenido en el Registro de desplazamiento de modo que está dispuesto a aceptar un nuevo dato. El flag UDRE0 se limpia automáticamente al ejecutarse la función de interrupción ISR. También se puede limpiar escribiéndole un uno. Interrupción de Transmisión Completada. Esta interrupción se habilita seteando el bit TXEN0 (además de I), y se disparará cuando se active el flag TXC0, es decir, cuando un dato se haya terminado de transmitir, o dicho de otro modo, cuando el dato haya salido por completo del Registro de desplazamiento. Creo que con eso queda clara su diferencia respecto de la Interrupción de Registro de Dato Vacío. En la práctica significa que los datos se envían más rápido usando la Interrupción de Registro de Datos Vacío porque los espacios entre datos se reducen al mínimo, de modo que la Interrupción de Transmisión completada será raramente usada. El flag TXC0 se limpia automáticamente al ejecutarse la función de interrupción ISR o puede limpiarse por software escribiéndole un uno.
Práctica: Interrupciones del USART Cada dato que llegue al USART es vuelto a enviar al terminal serial. Por otro lado, se envía todo un buffer (una cadena de texto) pero usando solo interrupciones. Usaremos el mismo circuito de la práctica anterior.
Circuito de la práctica. El código fuente /****************************************************************************** * FileName: main.c * Purpose: Uso de las interrupciones del USART * Processor: ATmel AVR con módulo TWI * Compiler: IAR-C y AVR-GCC (WinAVR) * Author: Shawn Johnson. http://www.cursomicros.com. * * Copyright (C) 2008 - 2012 Shawn Johnson. All rights reserved. * * License: Se permiten el uso y la redistribución de este código con * modificaciones o sin ellas, siempre que se mantengan esta * licencia y las notas de autor y copyright de arriba. *****************************************************************************/ #include "avr_compiler.h" #include "usart.h"
volatile char buf[] = "\n\r Uso de interrupciones del USART \n\r\r"; //**************************************************************************** // Gestor de interrupciones (Interrupción de Recepción Completada). // La interrupción de recepción se dispara cuando haya llegado algún dato. //**************************************************************************** ISR (USART0_RX_vect) { char c = UDR0; // Leer dato UDR0 = c; // Devolver dato } //**************************************************************************** // Gestor de interrupciones (Interrupción de Registro de Datos Vacío). // La interrupción se dispara cuando se pueda enviar un nuevo dato. // La ISR deposita en el registro de transmisión el siguiente dato a enviar. //**************************************************************************** ISR (USART0_UDRE_vect) { static unsigned char i = 0; char c = buf[i]; if(c != 0) // ¿ Fin de buffer ? { UDR0 = c; i++; } } int main(void) { usart_init();
// Inicializar USART0 @ 9600-8N1
/* Habilitar las interrupciones de 'Recepción Completada' y * de 'Registro de Datos Vacío'. */ UCSR0B |= (1<
Observa que en la función principal main no se transfiere ningún dato. Todos viajan por interrupciones. La interrupción de recepción se usa con mucha frecuencia y es la más fácil de captar. //**************************************************************************** // Gestor de interrupciones (Interrupción de Recepción Completada). // La interrupción de recepción se dispara cuando haya llegado algún dato. //**************************************************************************** ISR (USART0_RX_vect) { char c = UDR0; // Leer dato
UDR0 = c;
// Devolver dato
}
Eso es todo: cuando haya algún dato llegado lo recibimos y lo devolvemos :) La interrupción de transmisión no es tan usual como la anterior. Es una técnica algo sofisticada y muy eficiente pero que raras veces resulta realmente necesaria. En el programa funciona así: una vez habilitada, la interrupción se disparará de inmediato ya que el registro de transmisión UDR0 estará vacío. Así se empieza a enviar el primer dato de buf. La interrupción se volverá a disparar dada vez que el USART termine de enviar el dato anterior y parará en el último dato, cuando su flag se limpie por el solo hecho de empezar a ejecutarse la ISR, aunque en esa ocasión ya no se toque el registro UDR0. Fin de la historia. //**************************************************************************** // Gestor de interrupciones (Interrupción de Registro de Datos Vacío). // La interrupción se dispara cuando se pueda enviar un nuevo dato. // La ISR deposita en el registro de transmisión el siguiente dato a enviar. //**************************************************************************** ISR (USART0_UDRE_vect) { static unsigned char i = 0; char c = buf[i]; if(c != 0) // ¿ Fin de buffer ? { UDR0 = c; i++; } }
El buffer circular o buffer de anillo Recordemos que no podemos depositar un dato en el registro de transmisión del USART hasta que se termine de enviar el dato anterior. Mientras se espera a que eso pase el CPU podría perder tiempo valioso. La solución es guardar los datos en un buffer y que se vayan transmitiendo a su momento utilizando interrupciones, parecido a lo que se vio en la práctica pasada. Por otro lado, el registro de recepción, al ser un buffer de dos niveles, sí puede recibir un segundo dato antes de que se haya leído el dato previo. Pero si siguen llegando más datos sin que sean recogidos, no solo se perderían, sino que bloquearían el USART. Incluso si los datos se leyeran a tiempo utilizando interrupciones, ¿qué se haría con ellos si el CPU aún está ocupado procesando los datos previos? ¡Tienes razón! Podrían ir guardándose en un buffer. Pues bien, en ambos casos las cosas saldrán mejor si se usa un buffer de tipo circular. Un buffer circular es un recurso de programación tan viejo como útil en el intercambio de todo tipo de datos, no solo en comunicaciones del USART. No es más que un buffer o array ordinario que adopta su nombre por la forma en que se ponen y sacan sus elementos. Un buffer circular trabaja básicamente con dos índices, que aquí llamaremos Inpointer y Outpointer. No son como los punteros que define el lenguaje C, son simples variables que operan como índices para acceder a los elementos del buffer. Ambos índices tienen avance incremental y cíclico, es decir, se incrementan de uno en uno y luego de apuntar al último elemento del buffer vuelven a apuntar al primero. Eso explica su nombre.
Estructura de un buffer circular de N elementos. Al inicio los dos índices apuntan al primer elemento del buffer. La pregunta es ¿cuándo y cómo se incrementan?
Cada nuevo dato a guardar en el buffer será depositado en la casilla actualmente apuntada por Inpointer. A continuación Inpointer se incrementa en 1. (Inpointer para datos In = entrada.) Por otro lado, cada dato que salga del buffer será el de la casilla actualmente apuntada por Outpointer. A continuación Outpointer se incrementa en 1. (Outpointer para datos Out = salida.)
Con todo lo expuesto ya puedes ensayar cómo funciona el buffer circular. Descubrirás que tiene un comportamiento FIFO: los primeros datos en entrar serán los primeros en salir; que en tanto haya espacio en el buffer siempre se podrán meter más datos sin importar en qué posiciones vayan, evitando el riesgo de sobrescribir posiciones ya ocupadas. ¿Podrías hacer eso con un buffer lineal? Muy difícil, ¿verdad? Ahora pasemos de los ensayos a la práctica real. Para saber si en el buffer hay espacio para meter más datos o si hay al menos un dato que sacar, se debe usar la diferencia entre las posiciones de los punteros. Por lo confuso que se ve eso, es preferible emplear una variable adicional que se incremente con cada dato ingresado y se decremente con cada dato extraído.
Práctica: Buffer circular con Interrupciones El uso de un buffer circular en las recepciones de datos es una técnica robusta que se convierte en una necesidad de facto por razones ya explicadas. En las transmisiones, en cambio, brinda una eficiencia superflua y hasta innecesaria, salvo que la aplicación realmente la requiera. No quiero poner programas de esos aquí porque son demasiado grandes como ejemplos. Superficialmente esta práctica se ve igual que la primera de este capítulo. Solo que ahora todos los datos son transferidos por interrupciones pasando por buffers circulares.
Circuito de la práctica. El código fuente /****************************************************************************** * FileName: main.c * Purpose: Uso de buffers circulares con las interrupciones del USART * Processor: ATmel AVR con módulo TWI * Compiler: IAR-C y AVR-GCC (WinAVR) * Author: Shawn Johnson. http://www.cursomicros.com. * * Copyright (C) 2008 - 2012 Shawn Johnson. All rights reserved. * * License: Se permiten el uso y la redistribución de este código con * modificaciones o sin ellas, siempre que se mantengan esta * licencia y las notas de autor y copyright de arriba. *****************************************************************************/ #include "avr_compiler.h" #include "usart.h" #include "lcd.h"
char char void void
GetFromTXBuffer(void); GetFromRXBuffer(void); PutToTXBuffer(char data); PutToRXBuffer(char data);
#define #define
TXBufferSize RXBufferSize
50 50
// Tamaño de buffer circular de Tx // Tamaño de buffer circular de Rx
volatile volatile volatile volatile volatile volatile volatile volatile volatile volatile
char TXBuffer[TXBufferSize]; // Buffer circular de Tx char RXBuffer[TXBufferSize]; // Buffer circular de Rx unsigned char TXInpointer = 0; unsigned char RXInpointer = 0; unsigned char TXOutpointer = 0; unsigned char RXOutpointer = 0; unsigned char TXBufferData = 0; unsigned char RXBufferData = 0; unsigned char TXBufferSpace = TXBufferSize; unsigned char RXBufferSpace = RXBufferSize;
//**************************************************************************** // Gestor de interrupciones (Interrupción de Recepción Completada). // La interrupción de recepción se dispara cuando haya llegado algún dato. //**************************************************************************** ISR (USART0_RX_vect) { char c = UDR0; // Leer dato if(RXBufferSpace) PutToRXBuffer(c); else nop();
// Si hay espacio en RXBuffer // RXBuffer está lleno // Código para TXBuffer lleno
} //**************************************************************************** // Gestor de interrupciones (Interrupción de Registro de Datos Vacío). // La interrupción se dispara cuando se pueda enviar un nuevo dato. //**************************************************************************** ISR (USART0_UDRE_vect) { char c; if(TXBufferData) { c = GetFromTXBuffer(); UDR0 = c; }
// Si hay datos en TXBuffer // Estraer dato // Enviarlo
} int main(void) { char c; unsigned char i=0; const char *text = "\n\r Escribe en el LCD... \n\r"; usart_init();
// Inicializar USART0 @ 9600-8N1
lcd_init(); lcd_cmd(LCD_CURBLK);
// Inicializat LCD. Ver interface en "lcd.h" // Mostrar Cursor + Blink
while(c = text[i++])
// Cargar TXBuffer
{ if(TXBufferSpace) PutToTXBuffer(c); else nop();
// Si hay espacio en TXBuffer // Meter c // Código para TXBuffer lleno
} /* Habilitar las interrupciones de 'Recepción Completada' y * de 'Registro de Datos Vacío'. */ UCSR0B |= (1<= TXBufferSize) // Al pasar el límite TXOutpointer = 0; // Dar la vuelta TXBufferData--; // Un dato menos TXBufferSpace++; // Un espacio más return c; // } //**************************************************************************** // Extrae un dato de RXBuffer. // Antes de llamar se debe comprobar si hay algún dato con if(RXBufferData) //**************************************************************************** char GetFromRXBuffer(void) { char c = RXBuffer[RXOutpointer]; // Extraer dato if(++RXOutpointer >= RXBufferSize) // Al pasar el límite RXOutpointer = 0; // Dar la vuelta RXBufferData--; // Un dato menos RXBufferSpace++; // Un espacio más return c; // }
//**************************************************************************** // Ingresa un dato en TXBuffer // Antes de llamar se debe comprobar si hay espacio con if(TXBufferSpace) //**************************************************************************** void PutToTXBuffer(char data) { TXBuffer[TXInpointer] = data; // Ingresar dato if(++TXInpointer >= TXBufferSize) // Al pasar el límite TXInpointer = 0; // Dar la vuelta TXBufferData++; // Un dato más TXBufferSpace--; // Un espacio menos } //**************************************************************************** // Ingresa un dato en RXBuffer // Antes de llamar se debe comprobar si hay espacio con if(RXBufferSpace) //**************************************************************************** void PutToRXBuffer(char data) { RXBuffer[RXInpointer] = data; // Ingresar dato if(++RXInpointer >= RXBufferSize) // Al pasar pasar el límite RXInpointer = 0; // Dar la vuelta RXBufferData++; // Un dato más RXBufferSpace--; // Un espacio menos }
Descripción del programa
Como ves, hay dos buffers circulares, uno para transmisiones y otro para recepciones. Una vez implementados su uso es bastante simple. Solo compara esto: para enviar y recibir datos en un programa rústico se utilizan funciones como putchar para depositar un dato en el registro de transmisión, y getchar para leer del minibuffer de recepción de 2 datos. En cambio, con los buffers circulares podemos usar las funciones como PutToTXBuffer o GetFromRXBuffer para depositar/leer en/de sus “megabuffers” de transmisión y recepción. Se usan las variables como RXBufferData o TXBufferSpace para comprobar si hay datos o espacios para ellos en los buffers.