112

CONTENIDO - 148.206.53.84148.206.53.84/tesiuami/UAMI17156.pdf · Y FRENADO INTELIGENTE PARA CARRO A CONTROL REMOTO CAPITULO I: INTRODUCCION AL LENGUAJE C 9 Variable y tipos de datos

Embed Size (px)

Citation preview

2

CONTENIDO

Pág.

RESUMEN 4 INTRODUCCIÓN GENERAL DEL PROYECTO “SISTEMA DE APARCAMIENTO 5 Y FRENADO INTELIGENTE PARA CARRO A CONTROL REMOTO CAPITULO I: INTRODUCCION AL LENGUAJE C 9

Variable y tipos de datos. 9

Especificadores de tipo de datos. 10

Sentencias Selectivas. 11

Sentencias Iterativas. 14

Los operadores. 18

Las funciones. 23

Variables locales y variables globales. 27

Arrays. 30

Manejo de strings. 32

Configuración y Manejo de los puertos. 34 CAPITULO II: AVR STUDIO 5

Trabajando con Proyectos y Soluciones en C. 37

Creación de un proyecto en C. 37

Edición del Código Fuente. 40

Adición de Archivos o Librerías al Proyecto. 41

Construcción del Proyecto. 48

Renombrar los Archivos del Proyecto. 51

Cambiar la Frecuencia del Procesador. 52

Reconfiguración del Proyecto WinAVR. 53

Optimización del Código. 54

Cambiar de Microcontrolador AVR. 57

Simulación del Programa. 59

Los comandos de Depuración. 61

Las ventanas de Depuración. 63

Medición de Tiempos con Stop Watch 71

3

CAPITULO III. DISEÑO DEL SENSOR DE DISTANCIA

Introducción. 75

Consideraciones. 76

Diseño del Hardware y Software. 76 Diseño del Hardware. 76 Diseño del software. 79

Código Fuente. 80 CAPITULO VI. DISEÑO DEL CARRO A CONTROL REMOTO

Módulos de Radio Frecuencia (RF). 87 Módulos Receptores. 87 Módulos transmisores. 87

Características y funcionamiento de los módulos de RF. 88

Motor de DC. 90 Puente H. 90

PWM para controlar el puente H. 92

Diseño General Carro a Control Remoto. 92 Diseño del Hardware. 93 Diseño del software. 98 Código fuente. 99

CONCLUSIONES. 111

4

RESUMEN Este proyecto consta de 4 capítulos dentro del primero se da una breve introducción al Lenguaje de Programación C, con la intención de reafirmar sus conocimientos en el Lenguaje, dicha introducción al lenguaje está enfocada más a la programación de microcontroladores de la familia ATMEGA de AVR, es por ello que su contenido está limitado por el hecho de que este tipo de microcontroladores cuentan con baja potencia de procesamiento. En el segundo capítulo se da una descripción del uso de AVR Studio 5.0 que es un compilador con herramientas como editor de código, simulador de programa, depurador, entre otros bastante potentes, para desarrollar de forma integral nuestros proyectos. Más adelante en el tercer capítulo comenzamos con una introducción al diseño del sensor de distancia. Se detalla el funcionamiento y las características técnicas del sensor ultrasónico que utilizaremos, a continuación especificamos la parte del diseño el cual se divide en diseño a nivel Hardware y diseño a nivel software, en la primera parte se habla de los circuitos electrónicos que se construyeron y el funcionamiento de los mismos, la segunda parte está dedicada al diseño y codificación del programa para hacer el cálculo de la distancia en cm. Y terminamos el capítulo aportando nuestras conclusiones. Por último en el cuarto capítulo nos centramos en el diseño del Carro a Control Remoto, considerando en primer instante la forma en la cual los datos sean transmitidos y recibidos, en donde se optó por el uso de módulos de radiofrecuencia comerciales con frecuencia de trabajo de 433 MHz, damos un descripción de las características y funcionamiento de estos módulos y continuamos hablando un poco de los motores de DC, de la forma en que los vamos a controlar y los circuitos que son destinados a esta tarea como el puente H, planteamos por último el diseño del sistema y lo llevamos a cabo, de igual forma que en el capítulo anterior el diseño se divide en diseño a nivel Hardware y diseño a nivel Software, se presenta el código del programa y se finaliza con las conclusiones del capítulo.

5

INTRODUCCIÓN GENERAL DEL PROYECTO “SISTEMA DE APARCAMIENTO Y FRENADO INTELIGENTE PARA CARRO A CONTROL REMOTO. Una parte significativa de las personas en todo el mundo es dueña de coche o son conductores diarios. Entre estos factores, no es totalmente falso suponer que el estacionamiento en paralelo o el aparcamiento hacia atrás, es una de las partes más complicadas de su experiencia de conducción. Se necesitan años de experiencia de manejo y las prácticas rigurosas para evitar un choque de autos o una ralladura en el parachoques. Es por ello que este proyecto tiene como objetivo principal el desarrollo de un prototipo que realice dicha práctica sin la necesidad de que el conductor del vehículo efectúe acción alguna, para ello se utilizan sensores de ultrasonido para determinar la distancia que hay entre un objeto y el carro a control remoto. Se pretende diseñar un algoritmo que sea capaz de tomar las decisiones de en qué instante y bajo qué condiciones el carro a control remoto sea capaz de frenarse, avanzar y realizar las maniobras pertinentes que permitan que se estacione eficientemente. El proyecto está pensado para ir desarrollando primero los módulos secundarios y al final integrar todo al módulo principal una vez que se haya diseñado el algoritmo de control inteligente del aparcamiento. En el siguiente diagrama se muestra el diseño en general de nuestro sistema. Figura 0.1. Diseño de Alto Nivel del Proyecto.

Sensor de Distancia Parte Delantera

Control Inteligente

del Aparcamiento

Transmisor de RF

Carro a Control Remoto

Receptor de RF

Sensor de Distancia Parte Trasera

6

METAS A ALCANZAR Durante la primera etapa del proyecto se desarrollara un pequeño tutorial del lenguaje de programación C esto porque en el diseño y programación de sistemas como microcontroladores el tamaño de código fuente que se genera suele ser de gran tamaño es por ello que en este proyecto se determinó utilizarse un lenguaje de alto nivel, en vez de programar en ensamblador como anteriormente se hacía, además de que se simplifican mucho cálculos matemáticos al hacer uso de las librerías estándar de C. Se planea diseñar y construir un módulo que calcule la distancia entre el sensor y un objeto situado delante de él, para ello se eligió utilizar sensores ultrasónicos para calcular distancias, porque ultrasónicos porque aparte de ser económicos y fáciles de adquirir en el mercado tienen un alcance aceptable para los fines deseados. La segunda etapa del proyecto abarca el diseño, programación y construcción del carro a control remoto que servirá como base para integrar a él, el diseño del algoritmo para que este sea capaz de realizar maniobras para estacionarse en un cierto espacio. Justificación Los primeros dos capítulos de que trata este reporte son ambas herramientas que nos serán de utilidad para el desarrollo de nuestro proyecto, considero que es importante tomar en cuenta desde primer momento la elección del lenguaje de programación así como el Entorno de Desarrollo para llevar a cabo nuestro proyecto.

Los compiladores de alto nivel son las herramientas de programación más potentes que existen. 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, además de estar disponibles las librerías estándar de C que nos ofrecen grandes ventajas al hacer cálculos matemáticos. Esto simplifica de manera asombrosa el desarrollo de proyectos.

Los compiladores se encargan de traducir el código fuente al código objeto de cada microcontrolador sin importar mucho cuál sea. Por ejemplo, un código escrito para un PIC16F84 podría ser fácilmente compilado para un PIC16F877A u otro, y viceversa. Inclusive es posible adaptar un código para un microcontrolador de otra marca, por ejemplo, de Frescales o Atmel. Eso se llama portabilidad.

7

¿Por qué C y no cualquier otro lenguaje?

Las características (muchas veces complejas) del C fueron ideadas para el trabajo con sofisticados proyectos, propios de las computadoras. Muchas de esas características ya no resultan tan ventajosas en el limitado hardware de los microcontroladores y se convierten en prescindibles. Mas sin en cambio muchas otras características que pueden ser tomadas para lo programación de microcontroladores lo convierten en una herramienta potente.

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.

Ahora hablando un poco del 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, megaAVR, 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. 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 adaptador hardware,

8

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).

Se ha elegido el uso del entorno por sus características, flexibilidad y comodidad y se promueve el uso del mismo, más aún porque este año se lanzó la nueva versión del AVR Studio que incluye nuevas características y se ha abierto al enfoque de programación orientada a objetos con el uso de C++, aunque no es el único que existe, para PICs también existe el Entorno llamado MikroC de Microchip que ofrece muchas características aceptables para el desarrollo de proyectos.

9

CAPITULO I. INTRODUCCIÓN AL LENGUAJE C. 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 o 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:

Tabla 1.1.

Tipo de dato Tamaño Rango

char 8 0 a 255 o -128 a 127

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 variar 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 especificadoressigned (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_typemyvar; 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.

10

Ejemplos. unsigned char d;// Variable para enteros de 8 bits sin signo

char b;// Variable de 8 bits (para almacenar

// caracteres ascii)

signed char c;// Variable para enteros de 8 bits con signo

int i;// i es una variable int, con signo

signed int j;// j también es una variable int con signo

unsigned int k;// k es una variable int sin signo

También es posible declarar varias variables del mismo tipo, separándolas con comas. Así nos ahorramos algo de tipeo. Por ejemplo: float área,side;// Declarar variables área y side de tipo float unsigned char a,b,c;// 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 más no su escritura. Ejemplos: const int a=100;// Declarar constante a

int b;// Declarar variable b

//...

b=a;// Válido

b=150;// Válido

a=60;// Error! a es constante

a=b;// 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

11

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.

Falsa Verdadera

Figura 1.1. 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.

… SentenciaA;

SentenciaB; SentenciaC;

expresion

SentenciaX;

12

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.

Figura1.2. 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)// Si expression es verdadera, ejecutar

{// este bloque

sentenciaB;

sentenciaC;

// ...

}

else// En caso contrario, ejecutar este bloque

{

sentenciaD;

sentenciaE;

// ...

}

sentenciaX;

// ...

Como ves, es bastante fácil, dependiendo del resultado se ejecutará uno de los dos bloques de la sentencia if – else, pero nunca los dos a la vez. La sentencia if – else – if escalonada Es la versión ampliada de la sentencia if – else.

… SentenciaA;

SentenciaD; SentenciaE;

expresion

SentenciaX;

SentenciaB; SentenciaC;

13

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;

}

elseif(expression_2)// En caso contrario y si expression_2 es

{// verdadera, ejecutar este bloque

sentencia3;

sentencia4;

}

elseif(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

Las “expresiones” se evalúan de arriba abajo. Cuando alguna de ellas sea verdadera, se ejecutará su bloque correspondiente y los demás bloques serán salteados. El bloque final (de else) se ejecuta si ninguna de las expresiones es verdadera. Además, si dicho bloque está vacío, puede ser omitido junto con su else. 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 código en C se usan las palabras reservadas switch, case, break y default. El siguiente esquema presenta tres case’s pero podría haber más, así como cada bloque también podría tener más sentencias. switch(expression)

{

caseconstante1:// Si expression = constante1, ejecutar este bloque

sentencia1;

sentencia2;

break;

caseconstante2:// Si expression = constante2, ejecutar este bloque

sentencia3;

sentencia4;

break;

14

caseconstante3:// 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...

Dondeconstante1, 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.) expresión puede ser una variable compatible con entero. No es una expresión que conduce a una condición lógica como en los casos anteriores. El programa solo ejecutará uno de los bloques dependiendo de qué constante coincida con expression. Usualmente los bloques van limitados por llaves, pero en este caso son opcionales, dado que se pueden distinguir fácilmente. Los bloques incluyen la sentencia break. ¿Qué es eso? La sentencia break hace que el programa salga del bloque de switch y ejecute la sentencia que sigue (en el boceto, sentenciaX). ¡Atento!: de no poner break, también se ejecutará el bloque del siguiente case, sin importar si su constante coincida con expression o no. 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 más 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.

15

Falsa Verdadero

Figura 1.3. Diagrama de flujo de las sentencia while. El bucle while en C tiene la siguiente sintaxis y se lee así: mientras (while) expression sea verdadera, ejecutar el siguiente bloque. sentenciaA;

while(expression)// Mientras expression sea verdadera, ejecutar el

// siguiente bloque

{

sentenciaB;

sentenciaC;

// ...

};// Este ; es opcional

sentenciaX;

// ...

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 figura 1.4., esto implica que el cuerpo o bloque de este bucle se ejecutará al menos una vez.

… SentenciaA;

SentenciaB; SentenciaC;

expresion

SentenciaX;

16

Verdadera Falsa

Figura 1.4. 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

… SentenciaA;

SentenciaB; SentenciaC;

expresion

SentenciaX;

17

Ahora veamos por partes cómo funciona: expression_1 suele ser una sentencia de inicialización. expression_2 se evalú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 coincidentes con la primera plantilla, pero también son menos frecuentes.

18

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, multiplicación y división, están los operadores de módulo, incremento y decremento.

19

Tabla 1.2

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.

++ Incrementar en uno

-- Decrementar en uno

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

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++;// Lo mismo que a = b; y luego b = b + 1;

a=++b;// Lo mismo que b = b + 1; y luego a = b;

if(a++<10)// Primero comprueba si a < 10 y luego

{// incrementa a en 1

// algún código

}

if(++a<10)// Primero incrementa a en 1 y luego

20

{// comprueba si a < 10

// algún código

}

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 las 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.

Tabla 1.3

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;// variable de 8 bits

int n;// variable de 16 bits

m=0x48;// m será 0x48

m=m&0x0F;// Después de esto m será 0x08

m=m|0x24;// Después de esto m será 0x2F

m=m&0b11110000;// Después de esto m será 0x20

n=0xFF00;// n será 0xFF00

n=~n;// n será 0x00FF

m=m|0b10000001;// Setear bits 0 y 7 de variable m

m=m&0xF0;// Limpiar nibble bajo de variable m

m=m^0b00110000;// Invertir bits 4 y 5 de variable m

m=0b00011000;// Cargar m con 0b00011000

m=m>>2;// Desplazar m 2 posiciones a la derecha

// Ahora m será 0b00000110

n=0xFF1F;

n=n<<12;// Desplazar n 12 posiciones a la izquierda

// Ahora n será 0xF000;

m=m<<8;// 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

21

resultado es 0x00. Por cierto, en el lenguaje C no existen operadores de rotación. Hay formas alternativas de realizarlas.

<< >>

Figura 1.5 .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.

Tabla 1.4.

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.

Tabla 1.5

Operador Acción

&& AND lógica

|| OR lógica

! Negación lógica

Ejemplos:

if(!(a==0))// Si a igual 0 sea falso

{

// sentencias

}

if((a<b)&&(a>c))// Si a<b y a>c son verdaderas

22

{

// sentencias

}

while((a==0)||(b==0))// Mientras a sea 0 ó b sea 0

{

// sentencias

}

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: object 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; // Declarar a

a+=50;// Es lo mismo que a = a + 50;

a+=20; // También significa sumarle 20 a

a*=2;// Es lo mismo que a = a * 2;

a&=0xF0;// Es lo mismo que a = a & 0xF0;

a<<=1;// Es lo mismo que a = a << 1;

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é.

23

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<c, ...

{

// ...

}

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_type1function_name(data_type2arg1,data_type3arg2,...)

{

// 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();

24

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 menor(intarg1,intarg2,intarg3)

{

int min;// Declarar variable min

min=arg1;// Asumir que el menor es arg1

if(arg2<min)// Si arg2 es menor que min

min=arg2;// Cambiar a arg2

if(arg3<min)// Si arg3 es menor que min

min=arg3;// Cambiar a arg3

return min;// Retornar valor de min

}

void main(void)

{

int a,b,c,d;// Declarar variables a, b, c y d

/* Aquí asignamos algunos valores iniciales a 'a', 'b' y 'c' */

/* ... */

d=menor(a,b,c);// Llamar a menor

// En este punto 'd' debería ser el menor entre 'a', 'b' y 'c'

while(1);// Bucle infinito

}

25

En el programa mostrado la función menor 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 menor 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 menor del siguiente modo:

d=menor(-15,100,5.124);// Llamar a menor

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 menor (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

26

función main, ella siempre será la primera en ejecutarse. Por eso su prototipo de función es opcional. #include <avr8/io.h>

voidfunc1(charm,longp);// Prototipo de función "func1"

charfunc2(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

}

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. #include <avr8/io.h>

Void main(void)

{

// Cuerpo de la función

// Desde aquí no se puede acceder a func1 ni func2 porque están abajo

}

void func1(char m,long p)

{

// Cuerpo de la función

// Desde aquí se puede acceder a main pero no a func2

}

char func2(int a)

{

// Cuerpo de la función

// 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 así void func1(char, long);

char func2(int);

27

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 local count

/* Este count no tiene nada que ver con el count

de las funciones inter o foo */

count = 0; // Acceso a count local

speed = 0; // Acceso a variable global speed

}

char foo(long count) // Variable local count

{

int vari; // Variable local vari

}

28

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; // Declarar variable a

a++; // Incrementar a

}

Cualquiera que haya sido su valor inicial, ¿crees que después de llamar a esta función 10 veces, el valor de a se habrá incrementado en 10?... Pues, no necesariamente. Cada vez que se llame a increm se crea a, luego se incrementa y, al terminar de ejecutarse la función, se destruye. Para que una variable tenga una locación de memoria independiente y su valor no cambie entre llamadas de función tenemos dos caminos: o la declaramos como global, o la declaramos como local estática. Los buenos programadores siempre eligen el segundo. Una variable se hace estática anteponiendo a su declaración el especificador static. Por defecto las variables estáticas se auto inicializan a 0, pero se le puede dar otro valor en la misma declaración (dicha inicialización solo se ejecuta la primera vez que se llama a la función), así: static int var1; // Variable static (inicializada a 0 por defecto)

static int var2 = 50; // Variable static inicializada a 50

29

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; // Declarar variable var

//...

var = 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.

30

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

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 compuesta 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.

Figura 1.6. Estructura de un array unidimensional de N elementos.

31

Declaración de arrays Para declarar un array unidimensional se utiliza la siguiente sintaxis:

data_typeidentifier[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 letters[10];// letters es un array de 10 elementos de tipo char

long HexTable[16];// HexTable es un array de 16 elementos de tipo long

int address[100];// 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]='a';// Aquí el índice es 0

letters[1]='b';// Aquí el índice es 1

letters[2]='c';// ...

letters[3]='d';//

letters[4]='e';

letters[5]='f';

letters[6]='g';

letters[7]='h';

letters[8]='i';

letters[9]='j';// Aquí el índice es 9

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

32

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";// Un array de 10 elementos

char msg[]="Hello";// 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:

Figura 1.7. Estructura de una cadena de texto.

Manejo de Strings El manejo de strings es indispensable dentro de cualquier aplicación, para que el sistema pueda interactuar con periféricos de entrada /salida de datos como los teclados, el display LCD, los módulos de LEDS de 7 segmentos, o cualquier dispositivo con interfaz serial

33

RS232, como sería por ejemplo una terminal remota o una computadora PC ejecutando el software de comunicaciones "Hyperterminal". El lenguaje ANSI C contiene numerosas funciones de librería para el manejo de strings, entre ellas: atoi( ), atof( ), atol( ), printf( ), sprintf( ), gets( ) strlen( ) y muchas otras, las cuales simplifican el trabajo del programador al realizar conversiones y otras tareas relacionadas con strings en forma inmediata. Especialmente útil para el manejo de strings es la función sprintf( ), usada principalmente para enviar información a dispositivos como los LCD. Funciones para manejar strings. Veamos las más utilizadas . atoi( ). La función convierte strings de números enteros, en este ejemplo "-2928" en su valor binario , al cual le asigna la variable de nombre "a" . atof( ). La función convierte strings de números de punto flotante, en el ejemplo "3.1416" en su valor binario , al cual le asigna la variable "b". char RIM[6]=”-2928”; //declaración de un arreglo “string-2

char REM[7]=”3.1416” //segundo arreglo

long int a; //declaración variable entera de 116 bits

float b; //variable de punto flotante

a= atoi(RIM); //Función que convierte un numero entero a un string

b=atof(REM);//Función que convierte un numero de p. flotante a string

Ahora veamos la función sprintf( ), que hace la conversión de un valor en formato binario a un string, es decir la función inversa a atoi( ) y atof( ). La función es de formato un tanto sofisticado. Para poder usarla, es necesario definir previamente un arreglo que en este ejemplo será de 7 elementos (array[7] ), y que servirá para almacenar el resultado de la función que es un string. También debemos declarar una variable de punto flotante que en este ejemplo es a=24.54

sprintf(array,"%5.2f",a);

Esta función significa: convierte la variable "a", que es de punto flotante (f), a un string que se almacenará en el arreglo de nombre "array". El formato será de 5 espacios, 2 de los cuales son la parte decimal y 3 la parte entera.

Otros formatos para la función sprintf( ):

sprintf(array,"%u",a): para variable entera (unsigned int) de 8 bits ó 16 bits

sprintf(array,"%lu",a): para variable entera (long unsigned int) de 16 bits ó 32 bits

sprintf(array,"%3u",a): para asignar 3 bytes en el string para la variable "a" en el arreglo; si la variable ocupa solo 1 ó 2 bytes, llena con espacios en blanco.

34

sprintf(array,"%03u",a): para asignar 3 bytes en el string para la variable "a"; si la variable ocupa solo 1 ó dos bytes, llena el resto con "0".

char array[7];

float a;

a=24.54;

sprintf(array, “%5.2f”, a);

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/SMCUCR, 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.

MCUCR JTD BODS BODSE PUD --- --- IVSEL IVCE

Figura 1.8. Formato del Registro MCUCR.

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.

PORTA PORTA7 PORTA6 PORTA5 PORTA4 PORTA3 PORTA2 PORTA1 PORTA0

PINA PINA7 PINA6 PINA5 PINA4 PINA3 PINA2 PINA1 PINA0

DDRA DDA7 DDA6 DDA5 DDA4 DDA3 DDA2 DDA1 DDA0

Figura 1.9. Registros implicados en la configuración del puerto A.

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.

35

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 son 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.

36

Tabla 1.6

Caso Bit en DDRx

Bit en PORTx

Bit PUD (en MCUCR)

Dirección de Pin

Pull-up Estado de Pin

1 0 0 X Entrada No Tri-Estado (Alta impedancia)

2 0 1 0 Entrada SÍ Alto, debido a la pull-up.

3 0 1 1 Entrada No Tri-Estado (Alta impedancia)

4 1 0 X Salida No Bajo (0 lógico)

5 1 1 X Salida No 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á pull-up 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.

37

CAPITULO II. AVR STUDIO 5. 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.

Figura 2.1. Creando un proyecto desde Start Page.

De cualquier modo llegaremos al mismo asistente, donde empezamos por seguir lo que indica la figura 2.2. El nombre y la ubicación del proyecto pueden ser los que desees.

38

Figura 2.2. 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. Es recomendable tener un proyecto por cada Solución, de modo que no hay que marcar la casilla indicada y se pueden tener todos los archivos del proyecto y de la Solución dentro de la misma carpeta sin que se confundan. 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.

39

Figura 2.3. 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.

40

Figura 2.4. 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 figura 2.5.. El código mostrado es de ejemplo y tiene el nombre de ledflasher3. Todavía no lo podemos compilar porque faltan agregar al proyecto los archivos mencionados en las directivas include. Hablamos de archivos como por ejemplo avr_compiler.h, usart.h y usart.c, que en pocas palabras son las librerías adicionales que ocupara el main de nuestro programa y es el linker del compilador quien se encarga de enlazarlas.

41

Figura 2.5. Edición del programa ejemplo 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. Ay que incluir 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. El archivo avr_compiler.h con el que se trabajó cuenta con ligeras modificaciones, podemos observar a detalle el código a continuación y compararlo con el que se encuentra en el paquete ASF para notar las diferencias.

42

/************************************************************************

********

*

* Este archivo implementa algunas macros que permiten a los compiladores

IAR-C y

* AVR-GCC trabajar con el mismo código para los microcontroladores AVR.

*

* Documentation

* For comprehensive code documentation, supported compilers, compiler

* settings and supported devices see readme.html

*

* Author

* Atmel Corporation: http://www.atmel.com \n

* Support email: [email protected]

*

* Revision: 613

* Date: 2006-04-07 14:40:07 +0200 (fr, 07 apr 2006)

*

* Copyright (c) 2008, Atmel Corporation All rights reserved.

*

* Redistribution and use in source and binary forms, with or without

* modification, are permitted provided that the following conditions are

met:

*

* 1. Redistributions of source code must retain the above copyright

notice,

* this list of conditions and the following disclaimer.

*

* 2. Redistributions in binary form must reproduce the above copyright

notice,

* this list of conditions and the following disclaimer in the

documentation

* and/or other materials provided with the distribution.

*

* 3. The name of ATMEL may not be used to endorse or promote products

derived

* from this software without specific prior written permission.

*

* THIS SOFTWARE IS PROVIDED BY ATMEL "AS IS" AND ANY EXPRESS OR IMPLIED

* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF

* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE EXPRESSLY AND

* SPECIFICALLY DISCLAIMED. IN NO EVENT SHALL ATMEL BE LIABLE FOR ANY

DIRECT,

* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES

* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR

SERVICES;

* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER

CAUSED AND

* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR

TORT

* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE

OF

* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

*************************************************************************

*****/

#ifndef COMPILER_AVR_H

43

#define COMPILER_AVR_H

/* Define la frecuencia de CPU (en Hertz) por defecto, si aún no ha

* sido definida. */

#ifndef F_CPU

#define F_CPU 8000000UL // XTAL de 8 MHz

#endif

#include <stdbool.h>

#include <stdlib.h>

#include <string.h>

#include <stdio.h>

/* Esta macro protegerá el código siguiente de las interrupciones. */

#define AVR_ENTER_CRITICAL_REGION( ) uint8_t volatile saved_sreg = SREG;

\

cli();

/* Esta macro siempre se debe usar en conjunción con

AVR_ENTER_CRITICAL_REGION

* para restaurar la habilitación de las interrupciones.

*/

#define AVR_LEAVE_CRITICAL_REGION( ) SREG = saved_sreg;

#if defined( __ICCAVR__ )

/* Habilitar las definiciones de bits en el archivo ioXXX.h

* También se puede configurar en el IDE de IAR Embedded Workbench */

#ifndef ENABLE_BIT_DEFINITIONS

#define ENABLE_BIT_DEFINITIONS

#endif

//#include <stdint.h>

#include <inavr.h>

#include <ioavr.h>

#include <intrinsics.h>

#include <pgmspace.h>

#ifndef __HAS_ELPM__

#define _MEMATTR __flash

#else

#define _MEMATTR __farflash

#endif

/* Ejecuta un delay de us microsegundos.

*

* La macro F_CPU debe estar definida previamente.

*

* El máximo delay posible es de 262.14 ms / F_CPU en MHz.

*

* Nota. Para el compilador IAR, F_CPU debe ser un múltiplo de 1000000UL

(1 MHz).

*/

#define delay_us( us ) ( __delay_cycles( ( F_CPU / 1000000UL ) * ( us )

) )

44

/* Directivas para interrupciones en IAR-C.

*

* Algunas directivas de preprocesador para declarar las Rutinas de

Servicio de

* Interrupción en el compilador IAR C.

* Esto requiere el uso de la directiva C99 _Pragma() en vez de #pragma

* que no se puede usar como una macro de sustitución.

*

* Nota. NO trate de reordenar estas macros porque solo funcionarán en

el orden dado.

*/

#define PRAGMA(x) _Pragma( #x )

#define ISR(vec) PRAGMA( vector=vec ) __interrupt void

handler_##vec(void)

#define sei( ) (__enable_interrupt( ))

#define cli( ) (__disable_interrupt( ))

/* Define la macro no operation. */

#define nop( ) (__no_operation())

/* Define la macro watchdog reset. */

#define watchdog_reset( ) (__watchdog_reset( ))

/* Define la macro sleep_enter. */

#define sleep_enter() __sleep()

#define sleep() __sleep()

#define INLINE PRAGMA( inline=forced ) static

#define FLASH_DECLARE(x) _MEMATTR x

#define FLASH_STRING(x) ((_MEMATTR const char *)(x))

#define FLASH_STRING_T char const _MEMATTR *

#define FLASH_BYTE_ARRAY_T uint8_t const _MEMATTR *

#define PGM_READ_BYTE(x) *(x)

#define PGM_READ_WORD(x) *(x)

#define pgm_read_byte(x) *(x)

#define pgm_read_word(x) *(x)

#define SHORTENUM /**/

/* ************************************ */

#define PROGMEM __flash

#elif defined( __GNUC__ )

#include <stdint.h>

#include <avr/io.h>

#include <avr/interrupt.h>

#include <avr/pgmspace.h>

#include <util/delay.h>

#include <avr/sleep.h>

/* Define la macro sleep_enter. */

#define sleep_enter() sleep_cpu()

#define sleep() sleep_cpu()

/* Define la macro delay_us. */

#define delay_us( us ) (_delay_us( us ))

45

#define INLINE static inline

/* Define la macro no operation. */

#define nop() do { __asm__ __volatile__ ("nop"); } while (0)

#define MAIN_TASK_PROLOGUE int

#define MAIN_TASK_EPILOGUE() return -1;

#define SHORTENUM __attribute__ ((packed))

#else

#error Compiler not supported.

#endif

#endif

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 para el puerto serie (con archivos usart.h y usart.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 cómo hacer esto con los archivos avr_compiler.h, usart.h y usart.c y el procedimiento 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.

46

Figura 2.6. 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 una forma alternativa, que se ilustra a continuación.

47

Figura 2.7. 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.

48

Figura 2.8. 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 Debug de su traducción al español es el modo de depuración y es el modo por defecto, es recomendable usarlo solo para simular nuestro proyecto porque de él se genera mayor cantidad de código ensamblador por ende es más pesado para los microcontroladores, una vez que nuestro programa funcione perfectamente ahora si es adecuado cambiarlo al modo Release. 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.

Figura 2.9. Elección entre los modos Debug y Release.

49

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.

Figura 2.10. Construcción del proyecto y de la solución

. 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.

50

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.

Figura 2.11. 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.

51

Figura 2.12. Los 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.

52

Figura 2.13. 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. En el archivo avr_compiler.h del paquete ASF la constante F_CPU se define a 11059200Hz. Pero en el caso del programa de ejemplo F_CPU está redefinida a 8 MHz (como se aprecia en el siguiente extracto de dicho archivo).

////////////////////////////////////////////////////////////////

#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

53

Figura 2.14. Establecimiento de la frecuencia de procesamiento.

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.

Figura 2.15. Acceso a las propiedades del proyecto.

54

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.

Figura 2.16. 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 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 el programa de prueba resultó en 3066 bytes.

Optimizar más (nivel O2). Con este nivel el programa de prueba resultó en 3060 bytes.

55

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, el 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 AVR/GNU C Compiler >Optimization. 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.

Figura 2.17. Cambio de la optimización del código.

56

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 figura 2.18.

Figura 2.18. Configuración del compilador

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.

57

Figura 2.19. 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. Nosotros no invocamos al archivo delay.h directamente, sino a través deavr_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.

Figura 2.20. Reelección del microcontrolador del proyecto.

58

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.

Figura 2.21. 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.

59

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:

Figura 2.22. Uso de Makefile externo en Studio 5.

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

60

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.

Figura 2.23. 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.

Figura 2.24. iniciar la simulación.

61

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.

Figura 2.25. Herramientas de depuración disponibles actualmente para el ATmega88P.

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.

62

Figura 2.26. 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á.

63

Figura 2.27. 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.

Figura 2.2.8. Ventanas del menú Debug >Windows.

64

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.

Figura 2.29. Ventana que muestra la lista de los puntos de ruptura.

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.

65

Figura 2.30. Ventana del procesador.

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 ha dicho pin del AVR.

66

Figura 2.31. Ventana de entrada/salida.

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.

67

Figura 2.32. Ventanas Watch; valores de variables. 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.

68

Figura 2.33. Ventana watch 1 variable effect es igual a 0;

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.

Figura 3.34. Ventana de Memoria; mapa de la memoria de programa.

69

Figura 3.35. Ventana de Memoria; mapa de la memoria de datos.

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.

70

Figura 3.36. Ventana Disassembly; código en lenguaje ensamblador.

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.

71

Figura 3.37. Venta de Registros; mapa de los archivos de registro.

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.

72

Figura 2.38. 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í.

73

Figura 2.39. 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í:

74

Figura 2.40. El Stop Watch ha medido 107.144ms y el Contador de ciclos computó 857 152 ciclos.

75

CAPITULO III. DISEÑO DEL SENSOR DE DISTANCIA. Introducción En este capítulo se describe el diseño y la implementación de un medidor de distancia utilizando Sensores ultrasónicos del tipo UCM-R40K1, que será el asistente para que nuestro prototipo (carro a control remoto) sea capaz de estacionarse en una determinada área. La teoría básica detrás del medidor de distancia es la navegación de sonido y Ranging (SONAR), técnica que se utiliza para encontrar la distancia y la dirección de un objeto remoto bajo el agua mediante la transmisión de ondas sonoras y la detección de los reflejos de la misma. En primer lugar, una serie de breves pulsos ultrasónicos son transmitidos utilizando un transductor en el cual las variaciones de tensión generan ondas de sonido. El pulso de transmisión se refleja en un objeto, y la onda reflejada es recibida por otro transductor que convierte las ondas sonoras en voltaje. La señal transmitida la conoceremos como el ping y la señal recibida como el pong. Entre el dispositivo y un objeto puede ser fácilmente calculada multiplicando el tiempo transcurrido con la velocidad del sonido. Nótese que el tiempo medido representa el tiempo que tarda un pulso para viajar a un objeto más el tiempo que toma para viajar hacia atrás al receptor. Por lo tanto, el tiempo medido se reduce a la mitad en el cálculo de la distancia apropiada: Distancia = (Tiempo transcurrido/ 2) * 340.29 m/s Donde 340.29 m/s es la velocidad del sonido en el aire a una temperatura de 15 °C. El Ultrasonido hace referencia a las frecuencias arriba de 20KHz (limite de sonido audible).Altas frecuencias tienen longitudes de onda cortas lo que hace al reflejarse en objetos esta pueda ser leída. Desafortunadamente frecuencias muy altas son difíciles de generar y leer. El ultrasonido es aplicado comúnmente en detectores de movimiento, medidores de distancia, diagnóstico médico, limpieza, pruebas no destructivas (para detectar imperfecciones en materiales), soldadura entre otras más. El tipo de sensor que se utilizo es el UCM-R4OK1 con características técnicas:

76

Características técnicas del sensor utilizado.

Figura 3.1. Unidad piezoeléctrica

Consideraciones Durante la fase de prueba de nuestro módulo transmisor y receptor, nos dimos cuenta de que el alcance efectivo de nuestro medidor de distancia es proporcional a la potencia de la señal transmitida. En un principio se prefirió un rango mayor de alcance, pero sabemos que al obtener el voltaje más alto posible usando amplificador operacional también se amplificará el ruido por lo que se optó por un circuito alterno poco usual pero funcional compuesto por un Timer 555 y un arreglo de inversores CMOS. El alcance efectivo de nuestro dispositivo era importante, pero obtener pulsos cuadrados más limpios fue de mayor prioridad. El nivel de voltaje de la señal transmitida puede ser amplificada hasta 18 V, pero que resultó en un aumento de ruido en la señal recibida por lo que, producía cálculos aleatorios en la distancia. Diseño del Hardware / Software Diseño del Hardware. El circuito electrónico está dividido en dos sub-circuitos. Circuito transmisor y el receptor. El esquema básico del circuito transmisor se muestra a continuación.

Figura 3.2. Diagrama a bloques de la Transmisión del pulso de ultrasonido.

77

Una serie de cinco impulsos cortos de 40KHz son generados por el microcontrolador, el transmisor está construido con un 555 e inversores CMOS, la razón de utilizar un 555 en vez de usar un amplificador operacional es que los inversores trabajan con 5V a la entrada por lo que usar un amplificador operacional provocaría rebasar ese límite y provocar un daño a los inversores mientras que del Timer 555 siempre obtendremos un voltaje de 5 volts por el hecho de que está configurado como detector de pulsos quiere decir que obtenemos a la salida lo que hay a la entrada este modo de operación a su vez aumenta la corriente para mantener bien alimentados a los inversores, se emplea la característica CMOS para ponerlos en paralelo y aumentar la potencia de transmisión. La señal acoplada entre las terminales positiva y negativa es desfasada 180º, por lo que el voltaje aplicado entre las terminales es el doble. El transductor ultrasónico convierte su voltaje de entrada en ondas de sonido y emite a una frecuencia de 40 kHz ellos. Cabe hacer notar que otra razón de utilizar este diseño es que queremos reducir el ruido en la señal transmitida como en la señal recibida, por lo que no se contempló el uso de Amp. Operacionales.

Figura 3.3. Circuito transmisor

Una vez que la onda transmitida choca con un obstáculo que está reflejada y recibida por otro transductor ultrasónico que funciona como un receptor.

Figura 3.4. Diagrama a bloques de la Recepción del eco .

78

Figura 3.5. Circuito receptor

En primer lugar, el transductor ultrasónico convierte la onda de sonido recibido en tensión. El receptor se compone de dos circuitos: amplificador de señal y un circuito de detección. La señal es recibida por el sensor receptor y amplificada 1000 veces (60dB) en dos pasos un amplificador por 100 (40dB) y un amplificador por 10 (20dB).La señal amplificada se introduce a continuación a un disparador Schmitt 74HC14N para producir una onda cuadrada limpia. Cualquier valor por debajo de la tensión de disparo (2,5 V) dio cero lógico (0V) y cualquier valor por encima de 2.5V dio lógico alto (5V). Nótese que la señal invertida desde el inversor se invierte de nuevo por el disparador de Schmitt. La salida del disparador de Schmitt se alimenta entonces en el pin 4 del Puerto D(INT 1) para el procesamiento y cálculo de la distancia microcontrolador. Nota: El circuito Receptor aún está en la etapa de pruebas ya que la señal recibida (Eco) presenta aún mucho ruido por lo que se han propuesto y probado varias configuraciones de este circuito, la propuesta inicial que se describe anteriormente funciona pero se tuvieron que agregar dos inversores Schmitt Trigger. Además se está pensando en incluir una etapa intermedia de un detector de señal entre el amplificador y los inversores Schmitt el detector de señal consiste de un rectificador medio puente y un comparador. El

79

voltaje de comparación estaría establecido en 700mv, debajo de este nivel se encuentra el ruido indeseable para nuestro circuito.

Figura 3.6. Circuito Comparador propuesto.

Diseño del software. El programa se dividió en tres procedimientos: 1) la interrupción ISR (TIMER0_COMP_vect)

La interrupción corrió a 40 kHz. Para lograr establecimos el preescaler a 1 en el reloj y poner OCR2 a 198 que es nuestro valor de comparación con el valor de timer cuando ambos valores sean idénticos se activara la interrupción del Timer0 en modo de comparación. La funcionalidad principal de la interrupción es para generar 5 pulsos a 40 kHz. Para ello hemos implementado un contador con el cual controlamos la salida de los pulsos cada vez que se ejecuta la subrutina de interrupción se complementa el pin 0 del Puerto A. Después de que cinco pulsos consecutivos se han emitido hay que esperar a que el pulso reflejado llegue si no hay pulso reflejado después de un tiempo de espera apropiado cuando el contador llegue a 350(4.375 ms), hay que restablecerlas variables para enviar impulsos de nuevo. El tiempo de espera es el equivalente al sonido que viaja a una distancia de 1,49 m, lo que significa que nos dio un rango de aproximadamente 70 cm. Además decidimos emitir una serie de cinco impulsos cortos en lugar de uno ya que entrega más potencia. Hemos probado con un intervalo de 1 a 8 pulsos, los mejores resultados se obtuvieron a partir de 5 impulsos, lo que significa que la señal recibida era más fuerte cuando se emiten impulsos de 5.

80

2) void main ( void ) La función principal comienza por la inicialización de todos los temporizadores, contadores y la inicialización y configuración del display de LCD, así como cualquier variable que sea necesaria. Por último tenemos un bucle while donde espera por cualquiera de las dos interrupciones que tenemos.

3) ISR(INT1_vect) Función de interrupción externa en ella hacemos el cálculo de la distancia cuando llega nuestro pulso reflejado por el Sensor Receptor y es limpiándose produce un flaco de subida que activa la interrupción externa INT1. Una vez que tenemos una lectura del contador pong_timer(pulso reflejado) se almacena en otra variable y se realizan las operaciones necesarias para calcular la distancia en cm. Desde que está ejecutando la interrupción a 40 kHz, esto significa que cada ciclo representa .425 cm (1/40000) * Sec. 340 m / seg). Multiplicando el número de ciclos por este factor y dividiendo por 2 (para dar cuenta de la distancia al objeto próximo) se calcula la distancia real. Con estos datos se corre un promedio móvil de 7. Nos decidimos por la obtención de un promedio porque de esa manera podemos reducir los errores en las lecturas. Elegimos 7 por la experimentación. Hemos probado un rango de 5 - 20 que ejecutan cálculos de la media, pero se observó que después de las 7 las mejoras en los cálculos comenzaron a ser insignificante. Por último no queda más que formatear el valor de la variable dist para poder desplegarlo en le display de LCD. Cada vez que se ejecuta esta subrutina se restablecen los valores de los contadores para realizar una nueva lectura.

Código Fuente. A continuación se muestra el código fuente comentado de nuestro Medidor ultrasónico de distancia. /****************************************************************************** * Nombre: Medidor ultrasónico de Distancia * Processor: ATmega32 * Compiler: IAR-C y AVR-GCC (WinAVR) * Autor: Rodríguez Barrera Héctor Iván *****************************************************************************/ #include "avr_compiler.h" #include "lcd.h" //Declaracion de variables y constantes unsigned char cont; unsigned char pulse; //variable usada como nuestro pulso de salida int total_count; //variable para contar el numero de muestras para obtener un promedio float dist=0; //variable que usada para guardar el valor de la distancia calculada float v1,v2,v3,v4,v5,v6,v7;//variables que utilizamos para guardar muestras de la distancia y obtener un promedio char buff[17]; //usamos como buffer para almacenar nuestro valor de la distancia como string

81

char sreg_temp; //Variable usada para resguardar el registro de estado una vez que se ejecuta una subrutina de interrupcion int pong_timer; //Varible para contar indirectamente el tiempo que tarda un pulso en reflejarse //Rutina de interrupcion externa en ella hacemos el cálculo de la distancia cuando llega nuestro pulso reflejado por el receptor y es limpiado //se produce un flaco de subida que activa la interrupcion externa ISR(INT1_vect) { sreg_temp=SREG; //Salva el registro de estado v1=pong_timer; //Asignamos el valor de pong_timer a v1 para efectuar el cálculo del

promedio

//conversión para simplificar un poco ,as el cálculo 12.5 microSec * 340 m/Sec = .425 cm v1=((v1)*(.425))/2.0;//distancia calculada en cm

cont=0;//Se restablece los valores de los contadores para volver a tomar una nueva lectura de la distancia

pong_timer=0; //Se van almacenando 7 lecturas de la distancia para después obtener la media v2=v1; v3=v2; v4=v3; v5=v4; v6=v5; v7=v6; total_count++;

if(total_count==7)//Después de tomar 7 muestras de la distancia se saca el promedio para tener mayor precisión ya k por la presencia de ruido

{ //suele haber lecturas erróneas dist=(v1+v2+v3+v4+v5+v6+v7)/7; //Expresion que calcula el promedio lcd_gotorc(2,1);// Cursor a fila 2 posición 0 sprintf(buff, "%5.2f",dist); //función que convierte la variable dist de tipo

flotante a tipo string y la almacena en la variable //de tipo arreglo string dándole formato con 5 cifras enteras y dos decimales lcd_puts(buff);// Enviar buffer a LCD total_count=0; //Restablece el contador a cero para que se efectué una nueva lectura de la distancia } GICR=0x00; GIFR=(1<<INTF1); SREG=sreg_temp; //Restaura el valor original del registro de estado } /****************************************************************************** * Gestor de Interrupción por Desbordamiento del Timer0. * Esta interrupción se dispara cada 12.5 µs exactamente. * Genera una señal PWM con una frecuencia de 40KHz *****************************************************************************/ ISR(TIMER0_COMP_vect) { sreg_temp=SREG; //Salva el registro de estado if(cont<10) //Con este condicional nos aseguramos de mandar 5 pulsos de longitud de 12.25us { pulse=(~pulse&(1<<0));// Cada vez que se entra a la interrupcion se complementa la variable pulso para generar nuestra señal de salida PORTA=pulse; //Salida del pulso de 40KHz }

82

if (cont==10) //cuando se han mandado los 5 pulsos ponemos a cero la variable pong_timer quien nos servirá como contador para después

{//para después determinar el tiempo que tardo es regresar el pulso transmitido pong_timer=0; GICR=(1<<INT1); //Se habilita la interrupcion externa para que cuando llegue el primer flanco de subida nos indique k el pulso }//ha sido reflejado cont++; //Este contador lo usamos para dos cosas primero para darnos cuenta de en qué momento se han trasmitido los 5 pulsos //y segundo para establecer el límite de tiempo que esperaremos por un pulso reflejado visto de otra forma también //con el establecemos la distancia máxima (aunque esta depende más del las características de los sensores) de la que queremos tener lectura if(cont==350) //Con este condicional establecemos el límite del tiempo que esperaremos por un pulso reflejado, con 470 la establecemos aprox. a 5.875ms { cont=0;//una vez que no hubo pulso reflejado se restablecen los contadores pong_timer=0; //lcd_gotorc(2,1); //lcd_puts("fuera de rango"); } pong_timer++; TIFR=(1<<OCF0); SREG=sreg_temp; //Restaura el valor original del registro de estado } //************************************************************************ // Función principal //************************************************************************ void main (void) { TCCR0=(1<<WGM01)|(1<<CS10);//Establece el modo de comparación del Timer0 CTC = 40 kHz OCR0=198; //Cada que el valor del Timer0 es 198 entra a la rutina de interrupcion, en este modo no es necesario //restablecer el valor del Timer0 ya que este modo de operación lo hace por si solo cada vez que se sale //de la subrutina de interrupcion TIMSK=(1<<OCIE0); //Activa la interrupcion del Timer0 en modo CTC DDRA=0x01;// PA0 salida de señal DDRD=0x00; //Puerto D configurado como entrada cont=0; //Inicialización de las variables pong_timer=0; v1=0; v2=0; v3=0; v4=0; v5=0; v6=0; v7=0; lcd_init (); //Función que inicializa el display de LCD lcd_gotorc (1,1); //posiciona el cursor en el renglón 1 y la posición 1 lcd_puts ("Sensor d distancia"); //función que despliega el mensaje que se le pasa como

parámetro MCUCR=(1<<ISC10)|(1<<ISC11); //Configuración de la interrupcion externa por flanco de

subida sei(); //Se habilitan las interrupciones globalmente TIFR=(1<<OCF0); //limpiamos la bandera de interrupcion del Timer0 en modo CTC while(1) { nop(); } }

83

Códigos de las librerías para controlar un display de LCD. /****************************************************************************** * 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) *****************************************************************************/ #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<<lcd_E)|(1<<lcd_RW)|(1<<lcd_RS); /* Secuencia de inicialización del LCD en modo de 4 bits*/ lcd_CTRLout &= ~((1<<lcd_E)|(1<<lcd_RW)|(1<<lcd_RS)); ldelay_ms (45);// > 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<<lcd_RS);// Para escribir en DDRAM o CGRAM else lcd_CTRLout &=~(1<<lcd_RS);// Para escribir en Registro de Comandos delay_us (5);// Permite actualizar el Puntero de RAM lcd_nibble (inst);// Enviar nibble alto lcd_nibble (inst<<4);// Enviar nibble bajo } //**************************************************************************** // Envía el nibble alto de 'nibble' al LCD. //**************************************************************************** void lcd_nibble(char nibble) { lcd_CTRLout &=~(1<<lcd_RW);// Establecer Modo de escritura lcd_DATAddr |=0xF0;// Hacer nibble alto de bus de datos salida lcd_DATAout = (nibble&0xF0)|(lcd_DATAout&0x0F);// Colocar dato delay_us (2);// tAS, set-up time > 140 ns lcd_CTRLout |= (1<<lcd_E);// Pulso de Enable delay_us (2);// Enable pulse width > 450 ns

84

lcd_CTRLout &= ~(1<<lcd_E);// lcd_DATAddr &= 0x0F;// Hacer nibble alto entrada } //**************************************************************************** // Lee un byte de dato del LCD. // Si RS = 1 se lee la locación de DDRAM o CGRAM direccionada actualmente. // Si RS = 0 se lee el 'bit de Busy Flag' + el 'Puntero de RAM'. //**************************************************************************** char lcd_read (char RS) { Char high, low; if(RS) lcd_CTRLout |= (1<<lcd_RS);// Para leer de DDRAM o CGRAM else lcd_CTRLout &= ~(1<<lcd_RS);// Para leer Busy Flag + Puntero de RAM lcd_CTRLout |= (1<<lcd_RW);// Establecer Modo Lectura lcd_DATAddr &= 0x0F;// Hacer nibble alto entrada delay_us (2);// tAS, set-up time > 140 ns lcd_CTRLout |= (1<<lcd_E);// Habilitar LCD delay_us(2);// Data Delay Time > 1320 ns high= lcd_DATAin;// Leer nibble alto lcd_CTRLout &= ~(1<<lcd_E);// Para que el LCD prepare el nibble bajo delay_us (2);// Enable cycle time > 1200 ns lcd_CTRLout |= (1<<lcd_E);// Habilitar LCD delay_us (2);// Data Delay Time > 1320 ns low=lcd_DATAin;// Leer nibble bajo lcd_CTRLout &= ~(1<<lcd_E);// return (high&0xF0)|(low>>4);// 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) {

85

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); }

/****************************************************************************** * 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) *****************************************************************************/ #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_E3// Pin Enable #define lcd_RW2// Pin Read/Write #define lcd_RS1// 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

86

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);

87

CAPÍTULO IV. DISEÑO DEL CARRO A CONTROL REMOTO. Módulos de Radiofrecuencia En la construcción de nuestro primer control remoto podemos elegir módulos comerciales de Rf de bajo costo y con muy buenas prestaciones entre ellos: Módulos receptores:

Figura 4.1. RWS-375-5 / RWS-375-6 Módulo Receptor de RF 418/433.92 MHz Circuito LC Sensibilidad máx.: -110 dBm Alimentación 4.9 a 5.1 VCC

Figura 4.2. RWS-374-5/RWS-374-6 Módulo receptor de RF 418/433.92 MHz Circuito LC Sensibilidad máx.: -106 dbm Alimentación 4.9 a 5.1 VCC

Módulos transmisores:

Figura 4.3.

TWS-BS-418 / TWS-BS3-C Módulo transmisor 418/433.92 MHZ 10 mW de potencia Alimentación 3 a 12 VCC

Figura 4.4. TWS433-9A Módulo transmisor 433.92 MHZ8 mW de potencia Alimentación 1.5 a 12 VCC

88

Características y funcionamiento de los módulos de RF. Para construir un control remoto necesitaremos básicamente un emisor o transmisor y un receptor, Estos módulos son compatibles entre sí por lo que podemos usar cualquiera de estos emisores con cualquiera de estos receptores SIEMPRE Y CUANDO elijamos el transmisor con la misma frecuencia de trabajo del receptor. Podríamos elegir el par RWS-374-5 / TWS-BS-418 pues ambos trabajan a 418 MHz Los circuitos receptores tienen un dato esencial que es la sensibilidad máxima que esta expresada en dBm. El dBm se define como el nivel de potencia en decibelios en relación a un nivel de referencia de 1 mW. El valor de dBm en un punto donde hay una cierta Potencia P se define mediante la siguiente fórmula:

Podemos despejar de la formula a la Potencia (P)sabiendo que si tenemos una sensibilidad de -106 dBm para el caso de los receptores RWS-374-5(418 MHz) y RWS-374-6 (433.92 MHz) obtenemos que la mínima potencia necesaria para captar la señal es de unos0.00000000002 W Con este dato podemos elegir un transmisor, en este caso el transmisor TWS-BS-418 (418MHz) tiene una potencia de 10 mW ósea 0.01 W más que suficiente para que nuestro receptor pueda recibir la señal. La potencia va de la mano con el alcance de la señal .supongamos a grueso modo que por cada metro la potencia de la señal se disipa una cierta cantidad, nuestro receptor va a poder captar hasta que la potencia sea como mínimo 0.00000000002 W ósea que transmitirá hasta que la distancia sea tal que la potencia sea superior o igual a está. Esto en la teoría no es exactamente así pero ilustra muy bien lo que quiero explicar. Bien una vez que elegimos un par de módulos RF (transmisor-receptor) podemos emprender el camino para construir nuestro control remoto. En nuestro caso para el desarrollo del proyecto se optó por elegir los módulos RWS-374-5 y TWS-BS-418, podremos ver la disposición de los pines de estos módulos.

89

Figura 4.5. Función de los Pines del módulo Receptor.

Figura 4.6. Función de los Pines del módulo Transmisor.

Características generales:

1. Frecuencia: Esto indica los rangos de frecuencia en los cuales trabajan los módulos.

2. Sensibilidad del receptor: La sensibilidad de recepción (Sensitivity), indica qué cantidad de señal (dBm) debe estar presente en un receptor inalámbrico para trabajar correctamente a una determinada velocidad de transmisión (bps). Cuanto menor sea este valor más sensible será y podrá recibir señales de potencias menores. Por lo tanto es una característica muy importante en cuanto al alcance. Es importante destacar que la unidad se expresa en dbm y que por cada 3dbm se necesita una potencia 2 veces menor.

3. Potencia de salida: Es la potencia en mW que el módulo es capaz de entregar a una carga de 50 ohms.

4. Distancia: Esto se refiere al alcance que se obtiene entre dos módulos del mismo modelo a una velocidad de transmisión determinada y con características de visión directa y adaptación de antena.

La distancia que una señal puede ser transmitida depende de varios factores.

Los principales son:

90

.-La potencia de transmisión.

.-Las pérdidas en el cable entre el transmisor y la antena.

.-Ganancia de la antena del transmisor.

.-La localización de las dos antenas (separación y obstáculos entre ellas)

.-Ganancia de la antena de recepción.

.-Las pérdidas en el cable entre el receptor y su antena.

.-Sensibilidad del receptor.

5. Max. Air rate: Esto indica la máxima velocidad de transmisión de datos que puede alcanzar el módulo a través del aire entre un módulo y otro. Se expresa en bps.

6. Max. Baude rate: Esto indica la máxima velocidad de comunicación a través de la interface con el microcontrolador o dispositivo de comunicación de datos y el módulo.

7. Interfaz: Es la forma de comunicación que puede tener el módulo con un dispositivo externo.

Normalmente los módulos son incluidos dentro de sistemas embebidos con microcontroladores o microprocesadores. Por lo tanto su entrada y salida de datos debe ser adaptable a circuitos de este tipo.

Motor de DC Puente H

El puente H es un circuito de control electrónico de dispositivos de alta corriente, particularmente donde se puede invertir la polaridad del dispositivo, por ejemplo, motores DC. El nombre proviene del hecho de que el circuito se ve típicamente como una letra "H

91

Operación de puente completo

Figura 4.7. Dirección de giro del motor según

Los interruptores se cierren. El circuito que se muestra tiene cuatro interruptores y un motor. Para aplicar un voltaje en el motor un par de diagonal de interruptores deben activarse. Dependiendo de qué par de interruptores se enciende el motor y girará de una u otra manera. Si los conmutadores superior e inferior (por ejemplo letra C y D) se encienden el motor no tendrá ninguna diferencia de voltaje a través de el por lo que no habrá giro. Si la parte superior e inferior de un solo lado (por ejemplo A y B) se enciende por accidente se provocara un corto circuito, por lo que hay que tener cuidado de esta situación. Habrá que agregar un poco de lógica combinacional para evitarlo. La forma de construir dicho circuito es usando un par de transistores PNP (o MOSFETS de tipo p) en la parte superior y un par de NPN (o MOSFETS de tipo n) en la parte inferior como se muestra en la siguiente figura.

Figura 4.8. Puente H operando con transistores

92

PWM para controlar el puente H PWM(modulación por ancho de pulso) es un método de controlar digitalmente una salida con una tensión variable que cambia de estado cero a uno y viceversa. Esto hace fácil hacer que un transistor conmute o cambie de estado de encendido a apagado. Los Microcontroladores más modernos tienen la capacidad de generar señales PWM de manera eficiente. En la siguiente figura se muestra los pulsos que debe haber en las bases de los transistores para poder operar en los modos de avance y retroceso del motor.

Figura 4.9. Modos de operación del motor.

Para proteger el puente H y el microcontrolador, insertamos la circuitería lógica adicional, que consta de dos compuertas NAND y tres inversores, para que el motor realice el avance o retroceso del carro sin que se produzca un corto circuito por el hecho de que una terminal del motor sea polarizada con +Vcc y tierra simultáneamente, cuando se manda la señal de pwm.

Diseño General Carro a Control Remoto.

En este proyecto, utilizamos la radio frecuencia para transmitir y recibir datos de un microcontrolador a otro, así como dos puentes H, uno se utiliza para controlar el motor de D.C. de avance/retroceso y otro se utiliza para controlar el motor de la dirección del carro izquierda/derecha.

Un potenciómetro que controla la velocidad que se conecta al pin de entrada del convertidor analógico digital del microcontrolador. La Interpretación del valor del potenciómetro se maneja con el puerto A para ser más específico con el pin ADC0. Tres interruptores se utilizan; dos para la dirección del carro izquierda o derecha y un último botón para de prueba para encender una luz.

Abajo está nuestro diagrama a bloques del diseño.

93

Figura 4.10. Diagrama a bloques del Carro a Control Remoto.

Diseño de hardware.

El sistema se divide en dos subsistemas el primero será el transmisor y el segundo el receptor:

Transmisor y receptor: La Conexión del receptor y el transmisor se hace según el diagrama del circuito que se muestra en la figura. La antena del receptor debe ser mayor a 13 cm. para poder tener una señal de entrada de mayor amplitud y con menos ruido, según las especificaciones del módulo receptor. Los módulos receptor y transmisor son capaces de enviar y recibir datos hasta a 4800 baudios utilizando el protocolo de comunicación serial asíncrono. Para la comunicación entre los dos Atmega, utilizamos la USART. Como se observa en la figura el pin RXD del microcontrolador receptor está conectado al pin data del módulo receptor de igual forma el pin TXD del transmisor está conectado al pin data del transmisor en ambos casos el pin data es el número 2.

Potenciómetro ADC

Atmega32

Transmisor de RF

Switches

Velocidad- Motor

Puente H

Puente H

Atmega

32

Dirección-Motor

Receptor de RF

94

Pin Función

1 GND

2 data

3

4 Vcc

5 Vcc

6 GND

7 GND

8 Antena

Figura 4.11.Diagrama de conexión del circuito receptor.

PB0/T0/XCK1

PB1/T12

PB2/AIN0/INT23

PB3/AIN1/OC04

PB4/SS5

PB5/MOSI6

PB6/MISO7

PB7/SCK8

RESET9

XTAL212

XTAL113

PD0/RXD14

PD1/TXD15

PD2/INT016

PD3/INT117

PD4/OC1B18

PD5/OC1A19

PD6/ICP120

PD7/OC221

PC0/SCL22

PC1/SDA23

PC2/TCK24

PC3/TMS25

PC4/TDO26

PC5/TDI27

PC6/TOSC128

PC7/TOSC229

PA7/ADC733

PA6/ADC634

PA5/ADC535

PA4/ADC436

PA3/ADC337

PA2/ADC238

PA1/ADC139

PA0/ADC040

AREF32

AVCC30

U1

ATMEGA32

R1

10k

R2

10k

R3

10k

X1CRYSTAL

C122p

C222p

Vcc

Vcc

POT

100K

Velocidad

R410k

Vcc

Vuelta izqVuelta der.Luzd

ata

95

Pin Función

1 GND

2 data

3 Vcc

4 Antena

Figura 4.12. Diagrama de conexión del circuito transmisor.

PB0/T0/XCK1

PB1/T12

PB2/AIN0/INT23

PB3/AIN1/OC04

PB4/SS5

PB5/MOSI6

PB6/MISO7

PB7/SCK8

RESET9

XTAL212

XTAL113

PD0/RXD14

PD1/TXD15

PD2/INT016

PD3/INT117

PD4/OC1B18

PD5/OC1A19

PD6/ICP120

PD7/OC221

PC0/SCL22

PC1/SDA23

PC2/TCK24

PC3/TMS25

PC4/TDO26

PC5/TDI27

PC6/TOSC128

PC7/TOSC229

PA7/ADC733

PA6/ADC634

PA5/ADC535

PA4/ADC436

PA3/ADC337

PA2/ADC238

PA1/ADC139

PA0/ADC040

AREF32

AVCC30

U1

ATMEGA32

X1CRYSTAL

C122p

C222p

R110k

D2DIODE-LED

R210k

vue

lta

PW

M

izq

/ d

er

ava

nce

/ r

etr

oce

so

da

ta

96

La velocidad del motor DC es controlada variando la corriente en las respectivas terminales del motor, esto se hace usando PWM. Elegimos PWM, ya que reduce el consumo de energía y la complejidad del hardware, ya que las formas de onda PWM se pueden generar fácilmente, por el hecho de que dicha función está incorporada en la mayoría de los microcontroladores.

Como ya se había mencionado se conectaron dos puentes H uno para controlar la velocidad (Avance/Retroceso) y otro para la dirección (Izquierda/Derecha), recordaran que también se habló de que el puente H por si solo provocaba corto circuito entre una terminal del motor para evitar esto se agregaron compuertas NAND e inversores, la lógica de estos se detalla en la tabla. Los circuitos de esta conexión se muestran en la figuras de abajo, la primera de ellas es para el puente H que controla el avance y retroceso , la segunda figura muestra el circuito del puente H para controlar la dirección.

Tabla 4.3

Avance/retroceso Pwm Colector Q1 Colector Q2 Colector Q3 Colector Q4

1 1 1 0 0 1

1 0 0 0 1 1

Tabla 4.2.

Avance/retroceso Pwm Colector Q1 Colector Q2 Colector Q3 Colector Q4

0 1 0 1 1 0

0 0 0 0 1 1

97

Figura 4.13. Diagrama de conexión del circuito Puente H del motor de Velocidad.

Figura 4.14. Diagrama de conexión del circuito Puente H del motor de Dirección.

Q1

TIP31

Q2TIP31

Q3TIP32

Q4

TIP32

R1

10k

R2

10k

R3

10k

R4

10k

D11N4001

D21N4001

D31N4001

D41N4001

1

2

3

1

2

3

1 2

1 2

1 2

Vcc

Avance / Retroceso

PWM

Q1

TIP31

Q2TIP31

Q3TIP32

Q4

TIP32

R1

10k

R2

10k

R3

10k

R4

10k

D11N4001

D21N4001

D31N4001

D41N4001

1

2

3

1

2

3

1 2

1 2

1 2

Vcc

Izquierda / Derecha

Vuelta

98

Diseño de software.

El microcontrolador transmisor realiza tres tareas principales; convierte la señal analógica de entrada generada por el potenciómetro a la señal digital. Dos subrutinas de rebote de dos botones de entrada (izquierda y derecha) y una rutina de interrupción externa para un tercer botón (luz) y el comando codificado mediante protocolo USART de comunicación.

La conversión digital analógica es una de las funciones del microcontrolador. Se utiliza una exactitud de 8 bits del ADC ya que sólo siete niveles distintos de entrada son necesarios (tres niveles de avance, tres niveles de retroceso y uno de parada). Se utiliza un variable de estado para indicar si alguno de los tres botones de entrada ha sido oprimido. Ambas tareas se ejecutan continuamente dentro un ciclo While dentro de la función principal. La instrucción rebote se realiza cuando el programa está esperando el resultado de la ADC para ahorrar tiempo de proceso.

Se decidió transmitir los datos continuamente para simplificar el diseño del software, aunque también se puede transmitir bit por bit la información. En la rutina de atención a una interrupción de transmisión completada, enviamos el comando escribiendo en el registro UDR. El comando es una palabra de ocho bits que se codifica de forma en que evita el funcionamiento de unos y ceros consecutivos durante mucho tiempo, ya que los módulos de RF se vuelven inestables cuando se envían varios unos consecutivos este hecho se notó en las pruebas de laboratorio y se atribuyó a que la potencia no es la suficiente en el módulo transmisor. El formato del comando de 8 bits se muestra a continuación.

--- Luz --- Avance/Retroceso Dirección vuelta velocidad1 velocidad0

Figura 4.15. Formato del comando.

El microcontrolador receptor decodifica el comando que es recibido por el pin RXD, al terminarse la recepción del dato se activa la interrupción de recepción completada el comando es leído del registro UDR controla los motores utilizando PWM según el comando.

También usamos un bit del comando para indicar que se debe encender una luz en el carro, este bit es solo de prueba más adelante tendrá un uso, de mayor importancia.

99

Código Fuente. A continuación se muestra el código fuente comentado de nuestro Control Remoto (módulo transmisor). /****************************************************************************** * Nombre: Control Remoto * Processor: ATmega32 * Compiler: IAR-C y AVR-GCC (WinAVR) * Autor: Rodríguez Barrera Héctor Iván *****************************************************************************/ #include "avr_compiler.h" #include "usart.h" //Estado de un botón #define NoPush1// no presionado #define MaybePush2//posiblemente presionado #define Pushed3 //Presionado #define MaybeNoPush4//Posiblemente no presionado //direccion #define avance1 //avance #define reversa0 //retroceso // Declaracion de las variables como globales unsigned char command, entvel, velmag, direccion, novuelta; unsigned char luz, izquierda, derecha; unsigned char PushState1,PushFlag1,PushState0,PushFlag0,PushStateL,PushStateR; //Rutina de atencion a la interrupcion de transmision de datos completada ISR(USART_TXC_vect) { UDR=command; //se carga el valor del comando al registro UDR PORTB=command; //despliega por el puerto B el dato que se envió } void initialize (void) { //Configura el puerto como entrada para recibir el valor del potenciómetro DDRA=0x00; //salida para mostrar el comando por medio de leds DDRB=0xff; PORTB=0x00; //interruptores de entrada DDRC=0x00; //configura el puerto de modo que el pin TXD sea de salida DDRD=0x03; //configuración de las interrupciones GICR=0x40; //se activan las interrupciones por flanco de bajada MCUCR=0x02; //Registro de máscara de interrupción (TIMSK) PushStateL=NoPush;//estado del botón izquierdo PushStateR=NoPush;//estado del botón derecho luz=0;

100

usart_init(); //rutina de inicialización de la USART

ADMUX=0x20; //comando que permite que el receptor pueda ajustar la ganancia command=0x55; //se envía el comando 8 veces por la USART mediante la función putchar, para ajustar la ganancia putchar(command); putchar(command); putchar(command); putchar(command); putchar(command); putchar(command); putchar(command); putchar(command); sei(); //habilitación global de las interrupciones } //rutina Antirebote boton izquierdo void debounce_left() { switch (PushStateL) { case NoPush: if((PINC&0x08)==0){ PushStateL=MaybePush; } else{ PushStateL=NoPush; } break; case MaybePush: if((PINC&0x08)==0) { PushStateL=Pushed; izquierda=1; } else{ PushStateL=NoPush; } break; case Pushed: if((PINC&0x08)==0){ PushStateL=Pushed; } else{ PushStateL=MaybeNoPush; } break; case MaybeNoPush: if((PINC&0x08)==0){ PushStateL=Pushed; } else{ PushStateL=NoPush; izquierda=0; } break; } } //Rutina Antirebote del botón derecho void debounce_right() { switch(PushStateR)

101

{ case NoPush: if((PINC&0x04)==0){ PushStateR=MaybePush; } else{ PushStateR=NoPush; } break; case MaybePush: if((PINC&0x04)==0) { PushStateR=Pushed; derecha=1; } else{ PushStateR=NoPush; } break; case Pushed: if((PINC&0x04)==0){ PushStateR=Pushed; } else{ PushStateR=MaybeNoPush; } break; case MaybeNoPush: if((PINC&0x04)==0){ PushStateR=Pushed; } else { PushStateR=NoPush; derecha=0; } break; } } ISR(INT0_vect){ //codifica la luz luz=~luz; if(luz){ command=command|0x10; } else{ command=command&0xef; } } void main(void) { initialize();//LLama a la rutina de inicialización del avr UCSRB|=(1<<TXCIE); //Habilita la interrupcion de transmision completada while(1) { ADMUX=0x20; //ajuste del convertidor analógico, modo de conversión diferencial ADCSRA|=0xc7;//ADC habilitado e inicio de conversión debounce_left(); //llama a la función antirebote del botón izquierdo debounce_right(); //llama a la función antirebote del botón derecho

102

//codifica direccion if(izquierda==derecha)command=command|0x04;//no vuelta, si ningún botón es presionado o a su vez ambos son presionados else { command=command&0xfb; //vuelta if(izquierda){ command=command|0x01;//direccion a la izquierda } else{ command=command&0xfe;//direccion a la derecha } } while(ADIF==0) {//Espera a que la conversión A/D sea completada } entvel=ADCH;//guarda el valor del ADC en la variable entvel //codifica la velocidad if(entvel>0xa3) { //direction=advance; command=(command|0x10); //codifica la magnitud de la velocidad if(entvel<0xcd)velmag=0; else if(entvel<0xef)velmag=2; else velmag=3; } else if(entvel<0x67) { //direccion=retroceso; command=(command&0xef); //codifica la magnitud de la velocidad if(entvel>0x3f)velmag=0; else if(entvel>0x0d)velmag=2; else velmag=3; } else { velmag=1; } command=(command&0xfc)|velmag; } }

A continuación se muestra el código fuente del programa Receptor (Carro a Control Remoto). Este fue nuestro código del diseño inicial, al hacer las pruebas de laboratorio se observó que había problemas en la recepción más específicamente cuando los datos eran recibidos por el módulo receptor de rf, estos llegaban con diferente ancho en los pulsos, por ejemplo para los bits con valor 1 la anchura variaba desde los 100µs hasta los 180 µs y los bits cero la anchura variaba desde los 260 µs hasta los 300µs, siendo ambos deberían tener la misma anchura. Esto provocó que los datos no sean muestreados correctamente por la USART, se trató de solucionar variando el Baud Rate del Receptor pero no dio resultado, por lo que se diseñó y programo, un código alternativo únicamente para la

103

recepción en donde lo que se hace es que en cada flanco de bajada o subida se arranca un contador y dependiendo el valor que hay ente cada flanco de subida y bajada es como se determina si el bit es un cero o un uno, varios ceros o unos consecutivos. Este código se muestra más adelante.

Código fuente del programa Receptor. /****************************************************************************** * Nombre: Receptor (Carro a control Remoto) * Processor: ATmega32 * Compiler: IAR-C y AVR-GCC (WinAVR) * Autor: Rodríguez Barrera Héctor Iván *****************************************************************************/ #include "avr_compiler.h" #include "usart.h" // Declaracion de las variables unsigned char comandAnt,command; unsigned char velocidad,velAnt; unsigned char dir,vuelta; unsigned char cont,reload; unsigned int time0,contStop; unsigned char avance; unsigned char luz,stop;//para controlar la velocidad y poder detener el carro //********************************************************** ISR(USART_RX_vect) { command=UDR;// Leer dato del registro UDR PORTB=command;//Despliega el dato recibido por el puerto B para confirmar que el dato se haya recibido correctamente } //********************************************************** void initialize(void) { //Entrada para el ADC DDRA=0x00; //configuración del puerto B como salida para desplegar el dato recibido PORTB=0xff; DDRB=0xff; //configuración del puerto C como salida para desplegar el dato recibido DDRC=0xff; //configuración del puerto D como Entrada/Salida, para enviar PWM y recibir los datos por la USART DDRD=0xfe; //Configuración del Timer1, usado para generar la señal PWM TCCR1A=0x91; TCCR1B=0x02; //configuración de la USART usart_init(); //inicialización de las variables comandAnt=0x55; dir=0xfb;

104

vuelta=0xf7; velAnt=1; velocidad=1; advance=0xef; contStop=0; stop=0; //configuración del convertidor analógico digital ADMUX=0x20; sei(); } //********************************************************** void changespeed (void) { //actualiza la velocidad velAnt=velocidad; //cambia la velocidad del carro de acuerdo el comando recibido if((advance!=0xef)) { switch(velocidad) { case 0: OCR1A=128;//PWM al 50% break; case 1: OCR1A=0;//PWM al 0% break; case 2: OCR1A=191;//PWM al 75% break; case 3: OCR1A=255;//PWM al 100% break; } } else OCR1A=0;// detiene el carro } //********************************************************** void main(void) { initialize(); while(1) { //comprueba si el comando es modificado if(comandAnt!=command) { comandAnt=command; //decodifica el comando velocidad=(command&0x03); advance=(command&0x10); if(command&0x08)dir=0xfb; else dir=0x04; if(command&0x04) vuelta=0xf7; else vuelta=0x08; luz=(command&0x40); //cambia la velocidad if(velocidad!=velAnt) { if(velocidad==0x01)stop=1; else{

stop=0

105

if (velocidad<velAnt) contStop=7000; } else contStop=0; changespeed(); } } //control de los motores PORTD=dir; PORTD=vuelta; PORTD=avance; //luz de prueba if(luz) { PORTC&=(~0X02); PORTC&=(~0X04); } else { PORTC|=0X02; PORTC|=0X04; } } }

Código fuente del programa alternativo para la Recepción de los datos de forma serial con diferente tamaño de los bits. /****************************************************************************** * Nombre: Receptor de RF * Processor: ATmega32 * Compiler: IAR-C y AVR-GCC (WinAVR) * Autor: Rodríguez Barrera Héctor Iván *****************************************************************************/ #include "avr_compiler.h" //DEclaracion de las variables unsigned char dato, status, contdato, n, timer, flanco; int main(void) { inicializa(); status=0x00; while(1) { if(contdato==0x08){ //compara si ya se recibieron los 8 bits salta a la //sig instrucción, si no salta a main contdato=0x00; //se borra contdato para recibir un nuevo dato PORTB=dato; //se despliega el dato por el puerto B una vez que se

termino la recepción } } }

106

void inicializa(){ //configuración de puertos de salida DDRB=0Xff; DDRD=0x00; //Configuración del Puerto que recibe los datos //y q emite el pulso de confirmación de errores contdato=0x00; flanco=0x00; //se borra la bandera para indicar que el primer flanco es de bajada //configuración de las interrupciones GICR=0x40; //se activan por flanco de bajada MCUCR=0x02; //Registro de máscara de interrupción (TIMSK) TCCR0=0x03; //TIMSK=0x02; //Registro de máscara de interrupción (TIMSK) sei(); } //Rutina de atencion a la interrupción externa para la recepción de los datos ISR(INT0_vect){ timer=TCNT0; TCNT0=0x00; switch(status) { case 0: status=0x01; //cambia el estado a databit

flanco=1;//se cambia la bandera para indicar que el próximo flanco es de subida

dato=0; //se limpia el registro para recibir el nuevo dato TCCR0=0X02;//Se arranca el timer MCUCR=0x03; //se cambia la interrupcion a flanco es de subida break; case 1: switch(flanco){ case 1: if(contdato==0){//si el contador es cero indica a recibirse los ocho bits de datos

//tomando en cuenta que después cio que es un 1 lógico se manda un cero

//por lo que es necesario hacer un ajuste if((58<timer)&&(timer<64)){

//512us<tiempo<462us un cero no se toma encuentra el bit cero que se recibe

contdato=1; } if((82<timer)&&(timer<90)){

// 656us<tiempo<720us dos ceros no se toma encuentra el bit cero que se recibe

contdato=1; contdato=2; } if((140<timer)&&(timer<146)){

//1120us<tiempo<1168us tres no se toma encuentra el bit cero que se recibe

contdato=1; contdato=3; }

107

}

else { //si el contador no es cero entonces no se hace ajuste solo se toma el tiempo de cada cero lógico

if(timer<39){//un cero 312us<tiempo contdato=contdato+1; if(contdato!=0x08){

//si el contador es 8 ya no recorre el dato e incrementa el contador

dato=dato>>1; //si no es ocho recorre a la derecha

el reg. dato } } if((58<timer)&&(timer<64)){

//512us<tiempo<462us dos ceros contdato=contdato+2; if(contdato!=0x08){

//si el contador es 8 ya no recorre el dato e incrementa el contador

dato=dato>>2; //si no es ocho recorre a la derecha

el reg. dato } } if((82<timer)&&(timer<90)){

//tres ceros 656us<tiempo<720us contdato=contdato+3; if(contdato!=0x08){

//si el contador es 8 ya no recorre el dato e incrementa el contador

dato=dato>>3; //si no es ocho recorre a la derecha el reg.

dato } } if((140<timer)&&(timer<146)){

//cuatro 1120us<tiempo<1168us contdato=contdato+ 4;

dato=dato|0x00; //hace la op. or para meter el bit

recibido en el reg. dato if(contdato!=0x08){

//si el contador es 8 ya no recorre el dato e incrementa el contador

dato=dato>>4; //si no es ocho recorre a la

derecha el reg. dato } } } flanco=0;

//se cambia la bandera flanco para indicar que es próximo flanco es de bajada

MCUCR=0x02; //se cambia la interrupcion a flanco es de bajada

break;

108

case 0: if(24<timer){

//un 1 lógico si 160us<tiempo contdato++; dato=dato|0x80;

//hace la op. or para meter el bit recibido en el reg. dato

if(contdato!=0x08){ //si el contador es 8 ya no

recorre el dato e incrementa el contador

dato=dato>>1; //si no es ocho recorre a la

derecha el reg. dato } } if((38<timer)&&(timer<45)){

//dos unos consecutivos 300us<tiempo<360us contdato=1+contdato;

//se suma un 1 al contador dato=dato|0x80; if(contdato!=0x08){

//si el contador es 8 ya no recorre el dato e incrementa el contador

dato=dato>>1; //si no es ocho recorre a la derecha

el reg. dato } contdato=1+contdato;

//se suma un 1 al contador dato=dato|0x80; if(contdato!=0x08){

//si el contador es 8 ya no recorre el dato e incrementa el contador

dato=dato>>1; //si no es ocho recorre a la derecha

el reg. dato } }

flanco=1; //se cambia la bandera flanco para indicar que es próximo flanco es de subida

MCUCR=0x03; //se cambia la interrupcion a flanco es de subida

break; } if(contdato==0x08){

//si el contador es 8 significa que ya se recepción ha sido completada y se reinician las variables para recibir un nuevo dato

status=0; flanco=0; MCUCR=0x02; } break; } //limpia registro de banderas de interrupcion externa GIFR=0x040; }

109

Al código anterior se le han hecho pruebas pero aún sigue habiendo inestabilidad en la recepción se tiene planeado realizar nuevas mediciones en el laboratorio del ancho de los pulsos tomando en cuenta más casos una vez terminado se harán modificaciones a la estructura del programa para someterlo a nuevas pruebas. Librerías que implementan funciones para la USART. /****************************************************************************** * FileName: usart.c * Overview: Implementa funciones para el USART0 en modo Asíncrono * Processor: ATmel AVR con USART0 * Compiler: IAR-C y AVR-GCC (WinAVR) * *****************************************************************************/ #include "usart.h" #ifdefined(__GNUC__) FILE_stream=FDEV_SETUP_STREAM((int(*)(char, FILE*))putchar,/* function to write one char to device */ (int(*)(FILE*))getchar,/* function to read one char from device */ _FDEV_SETUP_RW);/* flags, see below */ #endif //**************************************************************************** // Inicializa el USART0. //**************************************************************************** void usart_init(void) { /* Configurar la velocidad de transmision/Reecepcion al doble */ UCSRA=(1<<U2X); /* Habilitar módulos Receptor y Transmisor */ UCSRB=(1<<TXEN)|(1<<RXEN); /* Configurar baud rate 2400 */ UBRRL=0xCF; /* Configurar modo de operación Asíncrono y formato de frame a * 8 bits de datos, 1 bit de stop y sin bit de paridad. */ UCSRC=(1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0);//|(1<<UPM1)|(1<<UPM0); /* Para el compilador AVR GCC, asociar las funciones 'putchar' y 'getchar' * con las funciones de entrada y salida (como printf, scanf, etc.) de la * librería 'stdio' */ #if defined(__GNUC__) // fdevopen((int (*)(char, FILE*))putchar, (int (*)(FILE*))getchar); stdin=stdout=&_stream; #endif } //**************************************************************************** // 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((UCSRA&(1<<UDRE))==0); /* Colocar dato en el buffer de transmisión */ UDR=dato; return dato; } //**************************************************************************** // Recibe un byte de dato del USART

110

//**************************************************************************** int getchar(void) { /* Esperar a que haya al menos un dato en el buffer de recepción */ while((UCSRA&(1<<RXC))==0); /* Leer y retornar el dato menos reciente del buffer de recepción */ #if defined(__GETCHAR_ECHO__) return (putchar(UDR0)); #else return UDR; #endif } /****************************************************************************** * FileName: usart.h * Overview: Macros y prototipos de funciones para el USART0 en modo Asíncrono * Processor: ATmel AVR con USART0 * Compiler: IAR-C y AVR-GCC (WinAVR) * * * *****************************************************************************/ #include "avr_compiler.h" /* Usar kbhit para ver si hay algún dato en el buffer de recepción antes de * llamar directamente a la función getchar para evitar esperas innecesarias. */ #define kbhit() (UCSR0A&(1<<RXC0)) /* Descomentar el siguiente #define para que la funcion 'getchar' haga eco de * los caracteres recibidos. */ //#define __GETCHAR_ECHO__ /* Macros para AVR GCC */ #if defined(__GNUC__) #ifdef putchar #undef putchar #endif #ifdef getchar #undef getchar #endif #endif /* Definiciones de funciones */ void usart_init(void); int putchar(int); int getchar(void);

111

CONCLUSIONES Conclusiones de la primera Etapa del proyecto. Los avances de esta parte del proyecto son que se logró concluir el desarrollo del módulo que mide la distancia en cm entre un objeto y el sensor ultrasónico que se usó. Entre las dificultades que se encontraron en el desarrollo de esta etapa se encuentran: La parte más difícil de configurar fue el esquema de tiempo. La idea original era utilizar el Timer1 en modo Fast PWM para crear una señal de 40 kHz. También se planteó el uso del Timer0 para funcionar como contador, pero al final se decidió utilizar una sola interrupción, donde usamos el Timer0 en modo CTC, porque es más fácil su uso y configuración ya que solo había que establecer el valor de comparación de OCR0 y en automático cada que se salía de la interrupción se restablecía este valor y olvidábamos de hacerlo por medio de código, se utilizó para generar los pulsos de 40 kHz y mantener nuestro contador de tiempo.

Número de impulsos: Hay un comportamiento algo aleatorio en el sistema que se refiere al número de pulsos emitidos en un conjunto a veces es mejor utilizar 4 o 6, pero la mayoría de veces 5 da los mejores resultados. Este problema particular es difícil de explicar y sólo puede resolverse mediante la alteración del código y el establecimiento de los pulsos en el valor óptimo (el que da los mejores resultados para este caso particular)

Pongs que faltan: a veces nos perdemos Pongs porque el código no pasa por el condicional if lo suficientemente rápido. La consecuencia está en los límites de la desigualdad. Nuestra distancia mínima está limitada por la velocidad de nuestro código. Hemos intentado mantener el código lo más corto posible para bajar la frontera. Conclusiones de la Segunda Etapa del proyecto. Entre los avances de esta etapa se encuentran que se terminó la construcción de nuestro módulo transmisor, las pruebas con el circuito que controla los motores terminaron de forma correcta, se probó la parte del código que los controla y también finalizaron de forma correcta. Queda por aclarar que esta última etapa aun esta inconclusa y en fase de desarrollo, se tuvo un gran número de dificultades en su desarrollo entre las que destacan que no se tenía contemplado que el módulo receptor que se adquirió, no nos entregara los datos de forma íntegra ya que estos de cierta forma se veían distorsionados y por ende no cumplían con los estándares de comunicación serial asíncrona, como no se cuenta con los recursos suficientes se tiene pensado seguir el desarrollo del proyecto con los mismos

112

módulos, lo que ahora nos ocupara es resolver ese problema, ya se planteó una solución pero aún está en la fase de pruebas. Consideramos que el proyecto se encuentra a la mitad de su desarrollo por lo que este mismo seguirá en marcha hasta ser concluido, como se mencionó el módulo que controlara al carro aún no está terminado, la parte del receptor que es en donde se encuentran las fallas están en proceso de ser corregidas, por ultimo quedara por realizar el diseño del algoritmo que controlara al carro para que este sea capaz de estacionarse a esta parte o módulo se le integraran dos módulos que censan la distancia y el módulo que controla el carro a control remoto. Esta parte ya se había comentado en la introducción y volvemos a mostrar la parte del diseño general para que quede más claro.

Figura 4.10. Diseño de Alto Nivel del Proyecto.

El desarrollo de este proyecto nos aportó un gran número de habilidades en las que destacaron la capacidad de afrontar problemas de mayor complejidad y la capacidad de hacer una mejor planeación del mismo, así como también la habilidad de codificar nuestros programas de forma más eficiente al estar horas sentado frente a la computadora, además de los conocimientos adquiridos al realizar investigación en especial en la parte del control de los motores, aunque esta es solo una pequeña parte de lo que se estudia en electrónica de potencia, nos sirvió de mucho para futuros proyectos que incluyan control automatizado de sistemas.

Sensor de Distancia Parte Delantera

Control Inteligente

del Aparcamiento

Transmisor de RF

Carro a Control Remoto

Receptor de RF

Sensor de Distancia Parte Trasera