Ensamblador AVR

69
Ensamblador AVR El hardware del microcontrolador El concepto detrás del ensamblador es hacer accesible los recursos de hardware del procesador. Estos recursos son todos los componentes del hardware, como la unidad central de proceso (CPU) y su servidor de matemáticas, la unidad aritmético-lógica (ALU), las unidades de almacenamiento diversas (RAM interna y externa, el almacenamiento EEPROM), los puertos y las características de los bits de control del puertos, los temporizadores, los convertidores AD y otros dispositivos. El acceso al hardware es directo y no mediante drivers u otras interfaces que proporcionan los sistemas operativos. Es el control directo de la interfaz serial o el conversor AD, no hay una capa entre usted y el hardware. Como premios a sus esfuerzos, el equipo completo está a sus órdenes, no sólo la parte que el diseñador del compilador y el programador del sistema ofrecen para usted. ¿Cómo trabaja la CPU? Lo más importante para la comprensión del ensamblador es entender como funciona la CPU. La CPU lee las instrucciones de la memoria de programa (flash), las traduce y las ejecuta en varios pasos. En los AVR, las instrucciones están escritas como números de 16 bits en la memoria flash y se leen de allí (primer paso). El número leído a continuación se traduce (2º paso) como por ejemplo llevar el contenido de los registros R0 y R1 a la ALU (tercer paso), sumarlos (4º paso) y escribir el resultado en el registro R0 (5º paso). Los registros son simples almacenes de 8 bits de ancho de los que se puede leer o escribir y llevar directamente a la ALU. Algunos ejemplos de codificación de las instrucciones: Operación de laCPU Código binario Cod. Hex. Enviar a la CPU a dormir 1001.0101.1000.1000 9588 Sumar el registro R1 al registro R0 0000.1100.0000.0001 0C01 Restar el registro R1 del registro R0 0001.1000.0000.0001 1801 Escribir la constante 170 al registro R16 1110.1010.0000.1010 EA0A Multiplique el registro R3 con el registro R2 y escribir el resultado a los registros R1 (MSB) y R0 (LSB) 1001.1100.0011.0010 9C32 Por lo tanto, si la CPU lee en hexadecimal 9588 de la memoria flash, detiene su funcionamiento y no trae más instrucciones. No preocuparse, hay otro mecanismo necesario antes de que la CPU ejecute esto, y además, se puede despertar a la CPU. Ejecutando las instrucciones Si la CPU lee 0C01 hexadecimal, se suma el registro R1 al registro R0 y el resultado se escribe en el registro R0. Esto se ejecuta como se muestra en la imagen. 1

Transcript of Ensamblador AVR

Page 1: Ensamblador AVR

Ensamblador AVR

El hardware del microcontrolador

El concepto detrás del ensamblador es hacer accesible los recursos de hardware del procesador. Estos recursos son todos los componentes del hardware, como la unidad central de proceso (CPU) y su servidor de matemáticas, la unidad aritmético-lógica (ALU), las unidades de almacenamiento diversas (RAM interna y externa, el almacenamiento EEPROM), los puertos y las características de los bits de control del puertos, los temporizadores, los convertidores AD y otros dispositivos.El acceso al hardware es directo y no mediante drivers u otras interfaces que proporcionan los sistemas operativos. Es el control directo de la interfaz serial o el conversor AD, no hay una capa entre usted y el hardware. Como premios a sus esfuerzos, el equipo completo está a sus órdenes, no sólo la parte que el diseñador del compilador y el programador del sistema ofrecen para usted.

¿Cómo trabaja la CPU?

Lo más importante para la comprensión del ensamblador es entender como funciona la CPU.La CPU lee las instrucciones de la memoria de programa (flash), las traduce y las ejecuta en varios pasos. En los AVR, las instrucciones están escritas como números de 16 bits en la memoria flash y se leen de allí (primer paso). El número leído a continuación se traduce (2º paso) como por ejemplo llevar el contenido de los registros R0 y R1 a la ALU (tercer paso), sumarlos (4º paso) y escribir el resultado en el registro R0 (5º paso). Los registros son simples almacenes de 8 bits de ancho de los que se puede leer o escribir y llevar directamente a la ALU.

Algunos ejemplos de codificación de las instrucciones:

Operación de laCPU Código binario Cod. Hex.

Enviar a la CPU a dormir 1001.0101.1000.1000 9588

Sumar el registro R1 al registro R0 0000.1100.0000.0001 0C01

Restar el registro R1 del registro R0 0001.1000.0000.0001 1801

Escribir la constante 170 al registro R16 1110.1010.0000.1010 EA0A

Multiplique el registro R3 con el registro R2 y escribir el resultado a los registros R1 (MSB) y R0 (LSB)

1001.1100.0011.0010 9C32

Por lo tanto, si la CPU lee en hexadecimal 9588 de la memoria flash, detiene su funcionamiento y no trae más instrucciones. No preocuparse, hay otro mecanismo necesario antes de que la CPU ejecute esto, y además, se puede despertar a la CPU.

Ejecutando las instrucciones

Si la CPU lee 0C01 hexadecimal, se suma el registro R1 al registro R0 y el resultado se escribe en el registro R0. Esto se ejecuta como se muestra en la imagen.

1

Page 2: Ensamblador AVR

En primer lugar la instrucción (palabra de 16 bits) se lee de la memoria flash y es traducida al ejecutable. Eñ siguiente paso conecta los registros a las entradas de la ALU y suma su contenido. A continuación, el resultado se escribe en el registro.

Si la CPU lee 9C23 hexadecimal del flash, los registros R3 y R2 son muliplicados y el resultado se escribe en R1 (los 8 bits más altos) y R0 (los 8 bits más bajos). Si la ALU no soporta la multiplicación, no está equipada con el hardware para la multiplicación (por ejemplo en un Attiny13), la instrucción 9C23 no hace nada en absoluto, ni siquiera abrir una ventana de error.

En principio, la CPU puede ejecutar 65.536 (16 bits) instrucciones diferentes. No obstante, debido a que 170 deben escribirse en un registro específico, y además los valores son de entre 0 y 255 en cualquier registro de entre R16 y R31, la cantidad de instrucciones da 256*16=4.096 de los 65.536 teóricamente posibles. La instrucción de carga directa de una constante c (c7...c0) y los registros r (r3...r0, r4 son siempre 1 y no codificados), es codificado como esto:

Bit 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0

LDI R,C 1 1 1 0 c7 c6 c5 c4 r3 r2 r1 r0 c3 c2 c1 c0

Por qué esos bits se colocan así en la palabra de instrucción sigue siendo secreto de ATMEL.

La suma y resta requieren 32 * 32 = 1.024 combinaciones y los registros objetivo (target) R0...R31 (t4...t0) y los registros fuente (source) R0...R31 (s4...s0) son codificados así:

Bit 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0

ADD Rt,Rs 0 0 0 0 1 1 s4 t4 t3 t2 t1 t0 s3 s2 s1 s0

SUB Rt,Rs 0 0 0 1 1 0 s4 t4 t3 t3 t1 t0 s3 s2 s1 so

Por favor, no aprendan estas colocaciones de bits, no lo necesitará más adelante. Sólo hau que entender cómo se codifica y se ejecuta una palabra de instrucción.

Instrucciones en ensamblador

No hay necesidad de aprender la colocación de bits en los números de 16 bits porque en ensamblador utilizará abreviaturas llamadas mnemotécnicos para ayudar a la memoria. La representación hexadecimal 9588 en ensamblador es la abreviatura “SLEEP”, que es más fácil de recordar.

Sumar simplemente es “ADD”. Los dos registros que se suman están representados como parámetros. Simplemente escriba “ADD R0,R1”, que se traduce a la palabra de 16 bits 0C01. La traducción es realizada por el ensamblador. La CPU sólo entiende 0C01. El ensamblador traduce la línea a la palabra de 16 bits, que se escribe en la memoria flash. La CPU la lee de la memoria flash y la ejecuta. Cada instrucción de la CPU tiene su correspondiente mnemotécnico y viceversa. La capacidad de la CPU determina el alcance de las instrucciones que están disponibles en ensamblador. El lenguaje de la CPU es la base, la mnemotecnia sólo representa la capacidad de la propia CPU.

Diferencias con los lenguajes de alto nivel

En los lenguajes de alto nivel las construcciones no son dependientes del hardware o la capacidad de una CPU. Esas construcciones trabajan con procesadores diferentes si hay un compilador del lenguaje para las diversas familias de procesadores. El compilador traduce las construcciones del lenguaje al

2

Page 3: Ensamblador AVR

lenguaje binario del procesador. Un GOTO en Basic se parece a un JMP en ensamblador, pero hay una diferencia de concepto entre los dos. Una transferencia de código de un procesador a otro, sólo funciona si el hardware es capaz de hacer lo mismo. Si un procesador de una CPU no tiene acceso a un temporizador de 16 bits, el compilador de un lenguaje de alto nivel tiene que simularlo utilizando un temporizador de 8 bits y un código de consumo-tiempo. Si hay disponibles tres temporizadores y el compilador está escrito para sólo dos o uno, el hardware disponible no se utiliza, por lo que se depende totalmente de la capacidad del compilador, no de las capacidades de la CPU.

Otro ejemplo se muestra con la instrucción “MUL”. En ensamblador, el procesador de destino determina si se puede utilizar esta instrucción o se tiene que escribir una rutina de multiplicación. Si en un lenguaje de alto nivel se utiliza una multiplicación, el compilador inserta una biblioteca matemática que multiplica todo tipo de números, incluso si sólo tiene números de 8 bits y sólo con utilizar MUL le bastase. La librería le ofrece un número entero pequeño, una palabra larga y otras rutinas de multiplicación que no son necesarias. Un paquete completo de cosas que realmente no necesita, así que te quedas pronto sin memoria flash en un AVR muy pequeño y tienes que cambiar a un xmega de 35 pines con puertos no utilizados sólo por tener su gran librería con rutinas superfluas ocupando espacio en la memoria flash. Eso es lo que se obtiene a partir de un simple “ * ” sin ni siquiera pedirlo.

Ensamblador no es lenguaje máquina

Debido a que el ensamblador se acerca más al hardware que cualquier otro lenguaje, a veces se le llama lenguaje máquina. Esto no es correcto porque la CPU sólo entiende instrucciones de 16 bits en formato binario. La cadena “ADD R0,R1” no se puede ejecutar, y el ensamblador es mucho más simple que el lenguaje máquina. Las similitudes entre el lenguaje máquina y ensamblador son una característica, no un error.

Intérprete y ensamblador

Un intérprete traduce código cercano al lenguaje humano a código binario para la CPU. El intérprete tiene estas características:

– Primero lee la secuencia de texto “A = A + B” (nueve caracteres de un byte cada uno),– tira los cuatro espacios en blanco del texto,– localiza las variables A y B (ubicación en los registros o SRAM, la precisión, longitud, etc),– identifica el signo + como operador,– prepara una secuencia de máquina ejecutable que es equivalente a la formulación del texto.

En consecuencia, el código máquina resultante sería varias palabras largas, leer y escribir variables de y a SRAM, sumar enteros de 16 bits, guardar y restaurar registro de la pila, etc...

La diferencia entre el intérprete y el ensamblador es que después de ensamblar la CPU obtiene sus palabras ejecutables directamente. En la interpretación, la CPU pasa la mayor parte del tiempo realizando la tarea de traducción. La traducción puede requerir de 20 ó 200 pasos de CPU, antes de que se puedan ejecutar 3 ó 4 palabras. La velocidad de ejecución es muy lenta. Esto no es problema si hay una velocidad de reloj muy rápida, pero no es apropiado en situaciones de tiempo críticas, donde se requiere una rápida respuesta a un evento. Nadie sabe en que está ocupada exactamente la CPU y cuanto tiempo requiere.

No tener que pensar en problemas de tiempo conduce a la incapacidad del programador para resolver problemas de tiempo y esta ignorancia le mantiene incapaz de resolver estas cosas si es necesario.

3

Page 4: Ensamblador AVR

Lenguajes de alto nivel y ensamblador

Los lenguajes de alto nivel añaden ciertas capas de separación no transparente entre la CPU y el código fuente. Un ejemplo de concepto poco transparente son las variables. Las variables pueden almacenar un número, una cadena de texto o un simple valor booleano. En el código fuente, un nombre de variable representa un lugar donde se encuentra dicha variable y hay que declararla: el tipo, si es un número y su formato, una cadena y su longitud, etc.

Para aprender ensamblador solo hay que olvidar el concepto de variable de los lenguajes de alto nivel.Ensamblador sólo sabe de bits, registros, bytes y bytes de SRAM. La expresión variable no tiene ningún significado en ensamblador. Además, los términos relacionados como “tipo” son inútiles y no tienen tampoco ningún sentido aquí.

Los lenguajes de alto nivel requieren la declaración de variables antes de su primer uso en el código fuente, cómo por ejemplo, si es un byte (8 bits), palabra doble (16 bits), entero (15 bits más uno de signo), etc. Los compiladores de estos lenguajes tienen que ubicar las variables declaradas en algún lugar del espacio de almacenamiento disponible, incluyendo los 32 registros. Si esta posición se selec-ciona a ciegas por el compilador o si se utiliza alguna regla de prioridad, como lo hace el programador en ensamblador cuidadosamente, depende quizás del precio del compilador. El programador sólo puede tratar de entender que criterio utilizó el compilador cuando puso la variable. El poder de decisión se le ha dado al compilador que “libera” al programador de los problemas de decisión, pero lo convierte en un “esclavo” del compilador. En la instrucción “A = A + B” si A se define como un carácter y B como un número (por ejemplo 2), la formulación no es aceptada porque los caracteres no se pueden sumar con números. Los programadores de alto nivel creen que la verificación de tipos les previene de una programación sin sentido. La protección que el compilador proporciona en este caso mediante el error de tipo, es más bien inútil: la adición de 2 a la letra “F”, por supuesto, debe dar “H” como resultado. El ensamblador le permite hacer esto, pero no un compilador.El ensamblador le permite sumar y restar números como 7 o 48 a cada byte almacenado, no importa que tipo de cosas haya en el almacenamiento de bytes. Esto es decisión del programador, no de un compilador. Si cuatro registros representan un valor de 32 bits o cuatro caracteres ASCII, si esos cuatro bytes se colocan del más bajo al alto o viceversa o mezclados por completo, es sólo cuestión del programador. Él es el maestro de la colocación, nadie más. Los tipos son desconocidos, todo se com-pone de bits y bytes en el almacenamiento disponible. El programador no solo tiene la tarea de orga-nizar sino también la tarea de optimizar. Igual pasa con otras reglas. Olvídese de la mayoría de las reglas en ensamblador. Para programar ensamblador es preciso contar con algunas reglas también, pero diferentes: La mayoría son creadas por el programador para ayudarse a sí mismo. Así que usted puede programar como quiera, no lo que el compilador decida por usted o lo que los profesores teóricos creen que sería buenas reglas de programación. Los programadores de alto nivel son adictos a una serie de conceptos que se interponen en el camino de aprendizaje del ensamblador: separación en diferentes niveles de acceso en el hardware, controladores y otros interfaces. En ensamblador esta separación es una tontería. La separación les condiciona a numerosas soluciones si quieren hacerlo de una manera óptima. Incluso los programadores puristas de alto nivel rompen estas reglas. En ensamblador nada le impide ser creativo, tiene acceso total al hardware, a la memoria, cualquier cosa se puede cambiar. La responsabilidad es únicamente del programador que tiene que usar su cerebro para evitar conflictos de acceso al hardware. La otra cara de la falta de mecanismos de protección es la libertad de hacer lo que quiera y como quiera, irá desarrollando sus propias reglas para evitar errores de ejecución.

¿Que es lo que es realmente más fácil en ensamblador?

Todas las palabras y los conceptos que el programador de ensamblador necesita están en la hoja de datos del procesador: las instrucciones y la tabla de puertos. ¡Ya está! Con esto ya se puede construir

4

Page 5: Ensamblador AVR

algo. No son necesarios otros documentos. ¿Como se inicia el temporizador? (está escrito en “Timer.Start)“LDI R16,0x02” y “OUT TCCR0,R16”. ¿Cómo se reinicia a cero? "CLR R16” y “OUT TCCR0,R16", todo está en la hoja de datos. No hay necesidad de consultar una documentación más o menos buena de cómo el compilador define esto o aquello. No hay palabras especiales diseñados por el compilador y los conceptos que hay que aprender están todos en la hoja de datos. Si desea utilizar un contador de tiempo en el procesador para un fin determinado puede utilizar cualquier modo de los 15 posibles modos diferentes, no hay ninguna regla en la forma de acceder al contador de tiempo, para detenerlo o reiniciarlo, etc.

¿Que en un lenguaje de alto nivel es más fácil escribir “A = A + B” que "MUL R16,R17"? No mucho. Si A y B no se definen como bytes o si el procesador es muy pequeño y no entiende MUL, el simple MUL tiene que ser intercambiado con otro código fuente tal como fue diseñado por el programador de ensamblador o copiar y pegar y adaptarlo a sus necesidades. No hay ninguna razón para importar una librería opaca, simplemente descarte la pereza en su cerebro y comience a aprender.

El ensamblador le enseña directamente como funciona el procesador. Debido a que un compilador no se hace cargo de las tareas, usted es completamente el capitán del procesador. La recompensa por esto es que se le concede acceso a todo. Si lo desea puede programar una velocidad en baudios de 45,45 bps en el UART, un ajuste de velocidad que no le permite un pc con windows porque el sistema opera-tivo sólo permite múltiplos de 75 (¿porque? Debido a que históricamente los escritores de teletipos mecanicos tenían las cajas de engranajes diseñadas para hacer una selección rápida de 75 o 300 bps). Si además, desea un registro de 1 byte y medio en lugar de uno de 1 o de 2, ¿porque no programar su propio dispositivo con ensamblador? Quien es capaz de programar en ensamblador tiene una idea de todo lo que el procesador le permite. El procesador entero está a la orden del programador. De esta manera puede adoptar soluciones de alta sensibilidad de forma fina y estética.

Hardware para la programación en ensamblador AVRLa interfaz ISP (programación en sistema) le permite leer y escribir el programa en la memoria flash y en la EEPROM integrada. Esta interfaz trabaja en serie y solo necesita tres lineas de señal:

– SCK: Una señal de reloj para desplazar los bits que se escriben en la memoria en un registro de desplazamiento interno y para desplazar los bits que se van a leer de otro registro de desplaza-miento.

– MOSI: La señal de datos que envía los bits que se escriben en el AVR.– MISO: La señal de datos que recibe los bits leídos desde al AVR.

Estos tres pines se conectan internamente a la máquina de programación sólo si cambia el pin de RESET (a veces también llamado RST o restart) a cero. De lo contrario, durante el funcionamiento normal del AVR, estos pines son programables como lineas de entrada/salida como todos los demás.

Si desea utilizar estos pines para otros fines durante el funcionamiento normal y en el sistema de programación, tendrá que tener cuidado de que estos dos objetivos no entren en conflicto. En general, habrá que disociarlos por medio de resistencias o un multiplexor.

No es necesario pero si recomendable en el modo ISP (in-system-programming), que el hardware de programación suministre la tensión de alimentación. Esto requiere dos lineas adicionales entre el programador y el AVR: GND, Tierra común o polo negativo y VTG (target voltage), tensión de alimentación (por lo general +5.0 voltios). Esto suma 6 lineas entre el hardware

5

Page 6: Ensamblador AVR

programador y el AVR, y se llama conexión ISP6 según la definición de Atmel.

Los standards siempre tienen standards alternativos que se utilizaron anteriormente. Esta la base técnica que constituye la industria del adaptador. En este caso, el standard alternativo se diseñó como ISP10 y ha sido utilizado en la placa STK200, también llamada interfaz CANDA. Todavía es un stan-dard muy extendido e incluso la placa STK500 más reciente está equipado con él. El ISP10 tiene una señal adicional para un led rojo, que indicaría que el programador estaría haciendo su trabajo. Es una buena idea, solo tiene que conectar el led con una resistencia y la tensión de alimentación positiva. Herramientas para programar en ensamblador AVR

– El editor,– el programa ensamblador,– la interfaz de programación de chips y– el simulador.

Hay dos posibles vías:

1. Todo en un paquete y2. Cada tarea se realiza con un programa específico y los resultados se van almacenando en

archivos específicos.

Generalmente se elije la primera vía, pero para comprender el mecanismo subyacente, vamos a describir la 2ª vía, que muestra la forma de un archivo de texto con palabras de instrucción en la memoria flash.

Estructuración del código ensamblador

La estructura básica de un programa en ensamblador AVR consta de :

– comentarios,– información de encabezado,– código al inicio del programa y– estructura general.

ComentariosEl texto más útil en un programa de ensamblador son los comentarios. Si usted necesita entender código antiguo que escribió, a veces años antes, apreciará el tener algunas sugerencias y todavía mejor, lo que ocurre en cada linea de código. Si le gusta mantener sus ideas en secreto y ocultarlas contra si mismo y los demás, no use los comentarios. Un comentario empieza con un punto y coma. Todo lo que sigue por detrás en la misma linea será ignorado por el compilador. Si tiene que escribir un comentario de varias lineas, comience cada linea con un punto y coma. Cada programa en ensamblador debe comenzar así:

;; Click.asm, Programa para conmutar un relé de abierto a cerrado cada 2 segundos; Escrito por G. Schmidt, última modificación: 10/07/2001;

Pon comentarios en todas las partes del programa, ya sea en una subrutina completa o una tabla. En el comentario hay que hablar de las características de la subrutina y las condiciones previas necesarias

6

Page 7: Ensamblador AVR

para llamarla o ejecutarla. También se pueden mencionar los resultados de la subrutina en caso de que más tarde se puedan encontrar errores o para ampliarla después. Los comentarios de una sola línea se añaden mediante un punto y coma en la linea después del comando:

LDI R16,0x0A ;Aquí se carga algoMOV R17,R16 ;y se copia en otro lugar

Información de encabezado

En la parte superior o encabezado del programa debe ir el propósito y función del mismo, el autor, la versión y otros posibles comentarios. Después debería ir el tipo de procesador para el cual está escrito, las constantes pertinentes y una lista con los nombres de los registros. El tipo de procesador es espe-cialmente importante, ya que los programas en ensamblador no se suelen ejecutar en diferentes tipos de chips sin cambios. Las instrucciones no son entendidas igual por todos los tipos de chips. Cada dispositivo tiene diferentes características y capacidades de EEPROM y SRAM. Todas estas carac-terísticas especiales se incluyen en un archivo de encabezado que se denomina xxxxdef.inc, donde xxxx es el tipo de chip como 2313, tn2323 o m8515. Estos archivos están disponibles y los proporcio-na Atmel. Es un buen estilo de programación incluir este archivo al comienzo de cada programa:

.NOLIST ; No liste lo siguiente en el archivo de lista

.INCLUDE "m8515def.inc" ; Importación del archivo de definiciones

.LIST ; Cambiar a lista de nuevo

La ruta de acceso, donde se puede encontrar este archivo, es necesaria si usted no trabaja con el software estudio de ATMEL. Durante el ensamble, por defecto, se genera un fichero *.lst que lista los resultados. Este fichero podría ser muy largo si se incluye el fichero de encabezado. La directiva .NOLIST omite el listado hasta que no se vuelva a activar. Veamos brevemente el archivo de encabezado. En primer lugar este archivo define el tipo de procesador:

.DEVICE ATMEGA8515 ; El tipo de dispositivo de destino

La directiva .DEVICE hace que el ensamblador compruebe si están disponibles todas las instrucciones para este tipo de AVR. El resultado es un mensaje de error si se utilizan secuencias de código que no están definidas para este tipo de procesador. El archivo de encabezado define también los registros XH, XL, YH, YL, ZH y ZL. Esto es necesario si usted va a utilizar los punteros de 16 bits X, Y o Z para acceder a los bytes de mayor y menor peso (higher o lower) por separado. Todas las ubicaciones de los puertos también se definen en el archivo de encabezado. Un número hexadecimal indica donde se encuentra definido un puerto en el dispositivo. Los puertos se definen con los nombres que vienen en las hojas de daros para cada procesador. Esto también se aplica a los bits individuales de cada puerto. El acceso de lectura al bit 3 del puerto B se hace usando el nombre PINB3 tal como se define en la ficha técnica. En otras palabras, si usted olvida de incluir el archivo de encabezado se encontrará con un montón de men-sajes de error durante la ejecución del ensamblaje. Los mensajes de error que se produzcan no necesa-riamente tienen que estar relacionados con que falta el archivo de encabezado. Otras cosas que deberían estar al principio de sus programas son las definiciones de registros con los que trabaja, por ejemplo:

.DEF mpr = R16 ; Define un nuevo nombre para el registro R16

Esto tiene la ventaja de tener una lista completa de los registros y además poder ver los registros que todavía están disponibles sin utilizar. Cambiar el nombre de los registros evita conflictos en su uso y además los nombres son más fáciles de recordar.

Al principio del programa y más adelante hay que definir las constantes, especialmente las que tienen un papel relevante en diferentes partes del programa. Tales constantes, por ejemplo la frecuencia Xtal a la que debe ajustarse el programa, si utiliza en la placa la interfaz serie, se definen así:

7

Page 8: Ensamblador AVR

.EQU fq = 4000000 ; definición de frecuencia Xtal

Así, desde el principio del código fuente se puede ver rápidamente para que reloj se ha escrito el programa, mucho más fácil que buscar esta información perdida dentro de 1482 líneas de código fuente.

Lo que se debe hacer al iniciar el programa

Después de haber hecho la cabecera debe comenzar el código del programa. Al principio del código se debe colocar los vectores de reset -y de interrupción- (ver su función en la sección JUMP). A medida que se produzcan saltos relativos se deben colocar detrás las respectivas subrutinas de servicio de interrupción. En el caso de dispositivos Atmega con mayor memoria flash, las instrucciones de salto se pueden colocar aquí, pero siendo cuidadoso. Hay aquí un poco de espacio de sobra para algunas subrutinas antes de colocar el programa principal, que siempre comienza con la inicialización del puntero de pila, el establecimiento de los registros con los valores por defecto y la inicialización de los componentes de hardware que se van a utilizar. El código siguiente es específico para cada programa.

El Ensamblador

Tenemos un archivo de texto con caracteres ASCII. El siguiente paso es traducir el código de tal forma que pueda ser comprendido por el chip AVR. Esto se llama ensamblaje, que significa “poner las palabras de instrucción de forma correcta”. El programa que lee el archivo de texto y produce otro de salida se llama Ensamblador. En su forma más simple es una aplicación de línea de comandos que cuando se le llama recibe como argumentos la ruta del archivo de texto y algunos modificadores opcionales para comenzar a ensamblar las instrucciones que encuentra en el citado archivo. Si su editor de texto permite llamar a programas externos esto es una tarea fácil. Si no, es más conveniente escribir un pequeño archivo por lotes o script (de nuevo con un editor). Este archivo por lotes debería tener una línea como esta: PathToAssembler\Assembler.exe -options PathToTextfile\Textfile.asm

Al hacer click en el lugar del editor que llama al programa externo o en el archivo por lotes se incia el ensamblador de linea de comandos.

La ventana pequeña informa de proceso completo de traducción. En este caso no hay errores. Si se producen estos son notificados junto con su tipo y número de línea. El ensamblador ha producido una palabra de código resultante de la instrucción RJMP que se ha usado. El ensamblador de nuestro único archivo de texto

asm ha producido otros cuatro archivos, aunque no todos se aplican aquí.Uno de estos cuatro nuevos archivos, TEST.EEP,

8

Page 9: Ensamblador AVR

contiene lo que se va a escribir en la EEPROM del AVR. Esto no es muy interesante en este caso, ya que no se genera ningún contenido para la EEPROM, por lo que el ensamblador lo que hace es elimi-nar este archivo porque está vacío. El siguiente archivo, TEST.HEX, es más relevante ya que contiene las instrucciones con las que se va a programar el chip.Los números hexadecimales se escriben en forma ASCII especial, junto con la información de la dirección y una suma de comprobación, esto para cada linea. Este formato se llama intel hexadecimal, es muy antiguo y proviene del mundo de la informática.

El tercer archivo, TEST.OBJ, será presentado más adelante, este archivo es necesario para simular un AVR. Su formato es hexadecimal y definido por ATMEL.

El contenido de un editor hexadecimal se parece a esto. Atención, este formato de archivo no es compatible con el software de programación. El archivo obj sólo lo generan algunos ensambladores de Atmel, no espere estos archivos en todos los ensambladores.

El cuarto archivo, TEST.LST, es un archivo de texto. Su contenido se puede ver con un simple editor. El archivo muestra el programa con todas sus direcciones, instrucciones y mensajes de error en un formato legible. Puede necesitar este archivo en casos en los haya errores por motivos de depura-ción. Estos archivos de listado solo se generan si se incluye la opción corres-pondiente en la linea de comandos y si la directiva .NOLIST no lo suprime.

Programación del chipPara programar el código hexadecimal, tiene que disponer de él en un archivo de texto y de un software de programación AVR. Este software lee los archivos .HEX y transfiere su contenido ya sea bit a bit (programación serie) o byte a byte (programación en paralelo) a la memoria flash del AVR.

9

Page 10: Ensamblador AVR

La imagen sería un ejemplo, pero tenga en cuenta que la ventana que aparece es del software AVR ISP.exe, un programa histórico que ya no distribuye Atmel. Ahora hay otros programas similares. El software grabará nuestro código en la memoria de programa del chip. El hardware de programación y distintas alternativas de software para diferentes sistemas operativos están disponibles en internet. Mencionamos aquí PonyProg2000 como un ejemplo para la programación en paralelo o por el puerto de comunicación serial.

Simulación en el software Studio

Algunas veces, aunque el ensamblado no produzca errores, el código no realiza exactamente lo que debería cuando se graba en el chip. Testear el software directamente en el chip podría ser complicado, especialmente si usted tiene un hardware mínimo sin oportunidad de mostrar los resultados provisio-nales o las señales de depuración. En estos casos el paquete de software Studio de Atmel ofrece opor-tunidades ideales para la depuración. El código del programa puede ponerse a prueba paso a paso para ver los resultados.

Las imágenes que mostramos a continuación han sido tomadas de la versión 4 de Studio, que está disponible gratuitamente previo registro en el sitio web de Atmel. La aplicación Studio tiene todo lo necesario para desarrollar, depurar, simular y grabar sus programas de ensamblador en el tipo de AVR de su elección.

El primer cuadro de diálogo le pregunta si quiere abrir un proyecto ya en curso o si va a empezar uno nuevo. Después de marcar nuevo proyecto, el botón siguiente le lleva a la configuración de su nuevo proyecto. En este cuadro de diálogo selecciona “Atmel AVR Assembler” como tipo de proyecto, le asigna un nombre (en este caso test1) y después una ubicación para el proyecto donde se tiene acceso de escritura. El botón siguiente abre el cuadro de diálogo de selección de dispositivos.

10

Page 11: Ensamblador AVR

Como plataforma de depuración seleccionar AVR simulador o AVR simulador 2. Como dispo-sitivo, aquí fue seleccio-nado un Atmega8.

Se cierra esta ventana con el botón finalizar y se abre una gran ventana que contiene un montón de diferentes sub-ventanas.

La ventana de la izquierda le permite ver y manipular todos los archivos del proyecto. En el centro, la ventana del editor le permite escribir el código fuente (no se preocupe por los colores, el resaltado de sintaxis es agregado por el editor). En la parte inferior izquierda está la ventana de “construcción” donde deben aparecer los mensajes de error. En el lado derecho una rara ventana de entrada/salida con la parte inferior en blanco (la veremos más adelante).

Todas las ventanas se pueden redimensionar y desplazar independientemente por la pantalla.

11

Page 12: Ensamblador AVR

Después de escribir su programa completo en código fuente, vaya al menú construir (build), pinche en el submenú construir y el resultado se mostrará en la ventana de construcción.Asegúrese de leer todo el contenido de la ventana una vez ya que le brindará mucha información. Si ha echo todo correctamente aparecerá un circulo verde con un mensaje indicándoselo, de lo contrario aparecerá un circulo rojo.Después puede ir a los menús Debug, Ver, Herramientas y Procesador, cambiar su tamaño o posición, o modificar su contenido. Debería tener este aspecto:

La ventana del editor tiene ahora una flecha amarilla. Esta flecha apunta a la siguiente instrucción que se ejecutará (realmente es una simulación).

La ventana de procesador muestra el valor actual del contador de progra-ma (el programa se inicia en la dirección 0), el puntero de pila, un contador de ciclos y un cronó-metro. Si pulsa en el pequeño “+” a la izquierda de la palabra “Registers” se muestra el conte-nido de los 32 regis-tros (todos están vacíos al inicio de la simulación).

Ahora vamos a proceder con la primera instrucción. En el menú “Debug” y después “step into” o simplemente pulsando F11 se ejecuta la primera instrucción.

12

Page 13: Ensamblador AVR

La instrucción “ldi rmp,0b11111111” carga el valor binario 1111.1111 en el registro R16.La flecha amarilla ya ha avanzado una instrucción hacia abajo, se encuentra ahora en la instrucción OUT.En la ventana de procesa-dor, el contador de pro-grama y el de ciclos han avanzado uno a su vez.El registro 16, en la lista de registros, está en rojo y muestra el valor 0xFF en hexadecimal, que en binario es 1111.1111.

Avanzando un paso más se ejecuta la instrucción OUT (pulsando la tecla F11 por ejemplo) y muestra la siguiente información.

La instrucción “out DDRB,rmp” escribe 0xFF al puerto llamado DDRB. Ahora la acción se encuentra en la ventana I/O. Si se pulsa en el pequeño “+” a la izquierda de PORTB, se muestra el valor 0xFF en el puerto DDRB de dos formas diferentes: como 0xFF en la parte superior de la ventana y con 8 cua-draditos en negro en la parte inferior.

Pulsamos 2 veces F11 y se escribe 0x55 en el PORTB. Como era de esperar, hay cambios en el contenido del PORTB. Hay ahora cuatro casillas en negro y las otras cuatro en blanco.

13

Page 14: Ensamblador AVR

Otros dos F11 y se escribe 0xAA en el PORTB y se ha invertido el color en los cuadraditos. Es lo que se esperaba. Pero, ¿que ha ocurrido en PINB? Tiene los colores opuestos a PORTB, igual que los tenía el PORTB en el paso anterior. PINB es un puerto de entrada para los pines externos. Como la dirección de los puertos en DDRB están fijados para ser salidas, PINB sigue el estado de los pines en PORTB justo en ciclo más tarde. No hay nada mal aquí. Para comprobarlo, basta con pulsar F11 varias veces para comprobar que es correcto.

Esta es una pequeña muestra del software simulador. El simulador es capaz de mucho más, asi que debe ser aplicado extensivamente en los casos de errores de diseño. Investigue los elementos de los menús, hay muchas cosas que se pueden mostrar. Por el momento, antes de seguir jugando con el simulador, hay que aprender algunas cosas básicas del lenguaje ensamblador, así que dejaremos el software studio por un tiempo.

¿Qué es un registro?

Los registros son depósitos especiales con capacidad de 8 bits y se ven así:Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0

14

Page 15: Ensamblador AVR

Tenga en cuenta la numeración de los bits, el menos significativo es el cero (matemáticamente 2 0 = 1).

Un registro puede almacenar números del 0 al 255 sin signo, o números de -128 a 127 (números enteros con el bit 7 de signo). También puede alamacenar un valor que representa un carácter codificado en ASCII (por ejem. 'A'), o tan sólo 8 bits individuales que no tienen relación entre sí (por ejem. 8 banderas individuales, utilizadas para señalar 8 valores booleanos).

Las características especiales de los registros, comparándolas con otras formas de almacenamiento, son:

– están conectados directamente con la unidad central de proceso, llamada acumulador.– Pueden ser utilizados directamente en las instrucciones de ensamblador, ya sea como destino de

un resultado o para leer el contenido y efectuar un cálculo o transferencia.– Las operaciones con su contenido solo requieren una palabra de instrucción.

Hay 32 registros en un AVR. Originalmente su denominación va de R0 a R31, pero se les puede cambiar el nombre a otro más significativo con una directiva del ensamblador. Un ejemplo:

.DEF RegistroPreferido = R16

Las directivas de ensamblador siempre empiezan con un punto, a diferencia de las instrucciones y etiquetas que NUNCA empiezan con un punto. Tenga en cuenta que las directivas sólo tienen sentido para el ensamblador, no producen ningún código ejecutable en el chip de destino. El nombre “RegistroPreferido” no se mostrará en el código hexadecimal ensamblado, por lo que no podrá ser derivado de éste.

En lugar de utilizar el nombre de registro R16, ahora podremos utilizar nuestro propio nombre “RegistroPreferido” si queremos utilizar R16 en una instrucción. Tenemos que escribir un poco más de texto cada vez que usemos este registro, pero en cambio, tenemos una asociación de lo que podría ser el contenido de este registro.

El uso en la linea de instrucción:

LDI RegistroPreferido, 150

que significa cargar el número 150 inmediatamente en el registro R16 (LoaD Inmediate). LDI carga un valor fijo o una constante al registro. Tras el ensamblado o traducción de este código a binario o hexadecimal, el programa almacenado en el chip luce así:

000000 E906

Esto aparecerá en el listado, el archivo *.lst producido por el ensamblador que es un simple archivo de texto. Todos los números estan en formato hexadecimal. El primero (000000) indica la dirección en la memoria flash donde se escribe y el segundo (E906) es el código de la instrucción. E906 indica al procesador tres cosas diferentes en una sola palabra, aunque no lo vea directamente:

– Un código básico de instrucción de carga, sinónimo de LDI,– el registro de destino (R16), donde se va a escribir el valor 150, y– el valor de la constante (150).

No tiene que recordar todo esto. El ensamblador sabe como traducirlo para dar finalmente E906 y el AVR lo ejecutará.

15

Page 16: Ensamblador AVR

En una instrucción dos diferentes registros pueden desempeñar una función. Un ejemplo fácil de una instrucción de este tipo es la instrucción de copia, MOV. El nombre de esta instrucción induce a confusión porque el contenido de un registro no se puede mover (¿que quedaría en el registro si se mueve el contenido a otra parte?). Mejor debería llamarse COPY, ya que lo que hace es copiar el contenido de un registro a otro registro. De esta manera:

.DEF RegistroPreferido = R16

.DEF OtroRegistro = R15LDI RegistroPreferido, 150MOV OtroRegistro, RegistroPreferido

Las dos primeras líneas de este pedazo de programa son directivas que definen los nuevos nombres de los registros R16 y R15. Como se mencionó antes, estas dos lineas no producen código para el AVR. Las líneas con las instrucciones LDI y MOV producen el siguiente código:

000000 E906000001 2F01

que escribe el valor 150 en el registro R16 y copia su contenido al registro de destino R15. AVISO IMPORTANTE: El primer registro es siempre el registro de destino donde se escribe el resultado.

Esto es diferente a como se lee o escribe y puede confundir al principio, pero de izquierda a derecha o de derecha a izquierda, son simples convenciones.

Registros diferentes

El principiante que quiera escribir las instrucciones de arriba de esta manera:

.DEF OtroRegistro = R15LDI OtroRegistro, 150

Ha perdido. Solo los registros R16 a R31 cargan una constante de inmediato con la instrucción LDI. R0 a R15 no hacen esto. Esta restricción no es muy fina, pero no pudo evitarse durante el proceso de construcción del conjunto de instrucciones de los AVR. Sin embargo, hay una excepción a esta regla: el establecimiento a cero de un registro. La instrucción:

CLR RegistroPreferido

es válida para todos los registros.

Esta restricción, además de en la instrucción LDI, se da en las siguientes instrucciones:

ANDI Rx,K ; and-bit al registro Rx con un valor constante K,CBR Rx,M ; Borrar todos los bits del registro Rx y establecerlos al valor de la máscara constante M,CPI Rx,K ; Compara el contenido del registro Rx con un valor constante K,SBCI Rx,K ; Reste la constante K y el valor actual de la bandera de acarreo al contenido del

registro Rx y almacena el resultado en este último.SBR Rx,M ; Establecer a 1 todos los bits del registro Rx, que son 1 en la máscara constante M,SER Rx ; Establecer a 1, todos los bits del registro Rx (igual que LDI Rx,255),SUBI Rx,K ; Restar la constante K del contenido del registro Rx y almacenar el resultado en

este último.

En todas estas instrucciones, el registro debe ser cualquiera de entre R16 y R31. Si va a utilizar estas ins-trucciones, hay que seleccionar uno de estos registros para operar. Es más corto y más fácil de progra-mar. Esta es una razón más por la que se debe utilizar la directiva para definir el nombre de un registro.

16

Page 17: Ensamblador AVR

Registros de puntero

Una función extra muy especial se define para los pares de registros: R27:R26, R29:R28 y R31:R30.El papel es tan importante que estas parejas tienen nombres cortos extra en ensamblador AVR: X, Y y Z. Estos nombres cortos son entendidos por el ensamblador. Estos pares son registros de puntero de 16 bits, capaz de apuntar a las direcciones con un máx. 16 bits de longitud, por ejemplo en lugares de SRAM (X, Y o Z) o en lugares de memoria de programa (Z).

Acceder a posiciones de memoria con punteros

El byte más bajo del puntero de 16 bits se encuentra en el registro inferior y el más alto en el registro superior. Ambas partes tienen su propio nombre. El byte más alto de Z se denomina ZH (=R31) y el más bajo es ZL (=R30). Estos nombres están definidos en el ensamblador. Dividir una palabra de 16 bits constante en sus dos bytes y escribir estos en un registro de puntero se hace como sigue:

.EQU address = RAMEND ;RAMEND es la dirección de 16 bits más alta de la memoria SRAM, que ;está definida en el correspondiente archivo de cabecera *def.inc,

LDI YH,HIGH(dirección) ;Cargar el MSB (byte más significativo) de la 'dirección'LDI YL,LOW(dirección) ;Cargar el LSB (byte menos significativo) de la 'dirección'

El acceso a través de registros de puntero se programa con instrucciones diseñadas especialmente. El acceso de lectura se denomina LD (LoaD) y el acceso de escritura ST (Store). Ejemplos con el puntero X (del mismo modo se puede usar Y y Z para tal fin):

Puntero Secuencia ejemplos

X Lectura/escritura de dirección X, no cambia el puntero LD R1,X o ST X,R1

X+ Lectura/escritura desde/hacia dirección X e incremento del puntero después en uno

LD R1,X+ o ST X+,R1

-X Primero, disminuir el puntero en uno y lectura/escritura desde/hacia la nueva dirección, después

LD R1,-X o ST -X,R1

Lectura de la memoria de programa flash con el puntero Z

Sólo hay una instrucción para el acceso de lectura en el espacio de almacenaje del programa. Se define para el puntero Z y que se denomina LPM (carga de memoria de programa). La instrucción copia el byte de la dirección del programa Flash a Z y al registro R0.Como la memoria de programa está organizada por palabras (una instrucción en una dirección se compone de 16 bits o 2 bytes o 1 palabra), el bit menos significativo selecciona el byte inferior o superior (0 = byte bajo, 1 = byte alto). Debido a esto la dirección original se debe multiplicar por 2 y el acceso está limitado a 15 bits o 32 kB de memoria de programa. De esta manera:

LDI ZH,HIGH(2*address)LDI ZL,LOW(2*address)LPM

Después de esta instrucción, la dirección debe ser incrementada para apuntar al siguiente byte en la memoria de programa.Como se utiliza muy a menudo, se ha definido una instrucción especial de incremento de puntero:

ADIW ZL,1LPM

17

Page 18: Ensamblador AVR

ADIW (Add Immediate Word). De esta manera se pueden sumar hasta un máximo de 63. Hay que tener en cuenta que le ensamblador espera la parte baja, ZL, del registro de puntero como primer parámetro. Esto es algo confuso ya que la adición se hace como una operación de 16 bits. La ins-trucción complementaria, que resta un valor constante entre 0 y 63 al registro de puntero de 16 bits, se llama SBIW (SuBtract Immediate Word). ADIW y SBIW son válidas para los pares de registros de punteros X, Y y Z y para los pares de registros R25:R24 en tanto que no tienen un nombre extra y no se permite el acceso a posiciones de memoria de programa o SRAM. El par R25:R24 es ideal para manejar valores de 16 bits.Como el incremento después de la lectura es muy a menudo necesario, los nuevos tipos de AVR sopor-tan la instrucción

LPM R,Z+

Esto permite llevar el byte leído a cualquier ubicación R y auto-incrementa el puntero de registro.

Listas en la memoria flash de programables

Ahora que sabemos cómo leer desde la memoria flash es posible que desee colocar una lista de constantes o una cadena de texto. ¿Cómo insertar una lista de valores en la memoria de programa? Esto se hace con las directivas del ensamblador .DB y .DW con las que se pueden insertar listas de valores por bytes o por palabras. Organizar listas por bytes tiene este aspecto:

.DB 123,45,67,89 ;una lista de cuatro bytes, escritos en forma decimal

.DB "This is a text." ;una lista de caracteres de un byte, escrito como texto

Siempre se debe colocar un número par de bytes en cada línea. De lo contrario el ensamblador añadirá un byte cero al final, lo que podría no ser deseado.

Una lista similar de palabras se parece a esto:

.DW 12345,6789 ;una lista con dos palabras constantes

En lugar de constantes también se pueden colocar etiquetas (por ejemplo, los objetivos de salto) en esa lista, así:

Label1:[ ... aquí algunas instrucciones ... ]Label2:[ ... aquí algunas instrucciones ... ]Lista:.DW Label1,Label2 ;una lista de etiquetas por palabras

Las etiquetas deben comenzar en la columna 1 y tienen que terminar con ":". Tenga en cuenta que en la lectura de las etiquetas de la lista con LPM (y el incremento posterior del puntero) se obtiene primero el byte más bajo de la palabra y después el byte superior.

Acceso a los registros con punteros

Una aplicación muy especial para los registros de puntero es el acceso a los mismos registros. Los registros se encuentran en los primeros 32 bytes del espacio de direcciones del chip (desde la dirección 0x0000 a la 0x001F). Este acceso sólo es válido si tienes que copiar el contenido de algún registro hacia la SRAM o la EEPROM o leer estos valores desde ellas hacia algún registro. Es más común el uso de punteros para el acceso a tablas con valores fijos en el espacio de memoria de programa. Como ejemplo, una tabla con 10 valores diferentes de 16 bits, donde se carga el 5º valor a R25:R24, así :

18

Page 19: Ensamblador AVR

MyTable:.DW 0x1234,0x2345,0x3456,0x4568,0x5678 ;Valores de la tabla organizada.DW 0x6789,0x789A,0x89AB,0x9ABC,0xABCD ;por palabrasRead5: LDI ZH,HIGH(MyTable*2) ;dirección en la tabla al puntero ZLDI ZL,LOW(MyTable*2) ;multiplicado por 2 para el acceso byte a byteADIW ZL,10 ;puntero al quinto valor de la tablaLPM ;lectura del LSB de la memoria de programaMOV R24,R0 ;copia LSB al registro de 16 bitsADIW ZL,1 ;puntero al MSB en la memoria de programaLPM ;lectura del valor MSB de la tablaMOV R25,R0 ;copia MSB al registro de 16 bits

Este es sólo un ejemplo. Se puede calcular la dirección de la tabla en Z de un cierto valor de entrada, llevando los respectivos valores de la tabla. Las tablas pueden ser organizadas byte a byte y también por caracteres.

Recomendaciones de uso de los registros

Las siguientes recomendaciones, si se siguen, pueden decidir si usted es un programador en ensamblador eficaz:

– Definir nombres para los registros con la directiva .DEF, no utilizar nunca su nombre directo Rx.

– Si necesita acceso al puntero reserve los registros R26 a R31para tal fin.– Un valor de 16 bits situarlo mejor en R25:R24.– Si tiene que leer de la memoria del programa, por ejemplo tablas fijas, reserve Z (R31:R30) y

R0 para tal fin.– Si usted planea tener acceso a bits individuales dentro de ciertos registros (por ejemplo,

banderas de testeo), el utilice los registros R16 a R23 para tal fin.– Los registros necesarios para las matemáticas están mejor situados de R1 a R15.– Si tiene registros más que suficientes, coloque las variables en los registros.– Si se queda corto de registros, coloque tantas variables como sea necesario en SRAM.

Puertos¿Qué es un puerto?

Los puertos en los AVR son puertas de la unidad central de proceso a los componentes de hardware y software interno y externo. La CPU se comunica con estos componentes, lee de ellos o les escribe, como los contadores o los puertos paralelos, etc. El puerto más utilizado es el registro de banderas, donde se señalizan las operaciones y se leen las condiciones de bifurcación.

Hay 64 puertos diferentes, que no están físicamente disponibles en todos los tipos de AVR. Dependiendo del espacio de almacenamiento y otros componentes internos, los diferentes puertos están disponibles y accesibles o no. Los puertos que pueden ser utilizados en un determinado tipo de avr con su correspondiente procesador están especificados en las hojas técnicas de datos. Los Atmega y ATXmega ampliados tienen más de 64 puertos, pero el acceso a los puertos más allá de #63 es diferente (veáse más adelante).

Los puertos tienen una dirección fija, con la cual se comunica la CPU. La dirección es independiente del tipo de AVR. Así, por ejemplo, la dirección de puerto del puerto B es siempre 0x18 (en notación hexadecimal. 0x18 en decimal es 24). Usted no tiene que recordar estas direcciones de los puertos, ya

19

Page 20: Ensamblador AVR

que tienen un alias conveniente. Estos nombres se definen en los archivos ' include ' (archivos de cabecera) para los diferentes tipos de AVR y que son proporcionados por el fabricante. Los archivos incluyen una línea que define la dirección del puerto B de la siguiente manera:

.EQU PORTB, 0x18

Así que sólo tenemos que recordar el nombre del puerto B, no su ubicación en el espacio de entradas/salidas del chip. El archivo de inclusión 8515def.inc está involucrado en la directiva de ensamblador:

.INCLUDE "C:\algúnlugar\8515def.inc"

y los registros del 8515 también son definidos en este archivo.

Los puertos por lo general se organizan como números de 8 bits, pero también puede contener hasta 8 bits individuales que no tienen mucho que ver entre sí. Estos bits tienen un significado y tienen su propio nombre asociado en el archivo de inclusión, por ejemplo para habilitar la manipulación de un solo bit. La convención de nombre se da para no tener que recordar estos bits y su posición. En las hojas de datos se pueden encontrar las tablas de puertos.

El acceso de escritura a los puertos

A modo de ejemplo, el registro de control general del MCU, llamado MCUCR, consiste en un número de bits de control que controlan las propiedades generales del chip. He aquí los detalles del puerto MCUCR del AT90S8515, tomados de las hojas de datos del dispositivo (otros puertos son similares):

Es un puerto empacado de 8 bits de control con sus propios nombres (ISC00, ISC01, ...). Si quiere enviar a su AVR a un profundo sueño, tiene que mirar en su hoja de datos como configurar los bits implicados, Así:

.DEF RegistroPreferido = R16LDI RegistroPreferido, 0b00100000OUT MCUCR, RegistroPreferidoSLEEP

La instrucción OUT trae el contenido de RegistroPreferido, con el bit SE (Sleep-Enable-Bit) activado, al puerto MCUCR. El bit SE permite al AVR irse a dormir, cada vez que la instrucción SLEEP aparez-ca en el código. Todos los bits del puerto MCUCR también se pueden establecer con las instrucciones anteriores. Si el bit SM (Sleep-Mode) se establece a cero, el AVR entrará en un modo denominado “duermevela”: no se ejecutarán más instrucciones pero el chip reacciona todavía al temporizador y otras interrupciones de hardware. Estos eventos interrumpen el sueño del AVR si tienen que notificar a la CPU.

La formulación anterior no es muy transparente, porque "0b00100000" no es fácil de recordar, y no se ve fácilmente que se ha establecido en una de estas instrucciones. Por lo tanto, es una buena idea formular la instrucción LDI de la siguiente manera:

LDI RegistroPreferido, 1<<SE

20

Page 21: Ensamblador AVR

Esta formulación indica al ensamblador:

– Tomar un uno (“1”),– Leer la posición del bit SE, tal como se define en la lista de símbolos del archivo de cabecera

8515def.inc, que da un valor de 5 en este caso,– Desplazar (“<<”) el 1 cinco veces a la izquierda (“1<<5”) en los pasos:– 1) inicial: 0000.0001, 2) primer desplazamiento a la izda.: 0000.0010, 3) segundo desplaz. a la

izda.: 0000.0100 y así sucesivamente hasta 6) quinto desplaz. a la izda.: 0010.0000.– asociar este valor a RegistroPreferido e insertar esta instrucción LDI en el código.

¿Como se hace si se quieren establecer en la misma instrucción LDI, los bits SM y SE al mismo tiempo? SM=1 y SE=1 establecen al AVR en modo apagado (power down), por lo que sólo lleve esto a cabo si entiende cuáles son las consecuencias. La formulación es la siguiente:

LDI RegisroPreferido, (1<<SM) | (1<<SE)

Ahora, el ensamblador calcula primero el valor del primer tramo, (1 <<SM), un "1" se desplazó cuatro veces a la izquierda y se produce 0001.0000, a continuación, calcula el segundo tramo, (1 <<SE ), un "1" se desplaza cinco veces a la izquierda produciendo 0010.0000. El "|" entre los dos valores produce una operación BIT-OR ente ellos por cada bit uno a uno. El resultado de hacer esto con 0001.0000 y 0010.0000 da en este caso 0011.0000, que es el valor deseado para la instrucción LDI. A pesar de que la formulación:

(1<<SM) | (1<<SE)

podría, en el primer vistazo, no ser más transparente que el valor resultante:

0011.0000

para un principiante, es más fácil de entender que los bits del MCUCR están propuestos a ser manipula-dos con esta instrucción LDI. Especialmente si usted tiene que leer y entender el código algunos meses más tarde los bits SM y SE son una indicación de que aquí se está estableciendo el modo sleep o dor-mir. De otra manera, tendría que consultar el libro de datos del dispositivo con mucha más frecuencia.

El acceso de lectura a los puertos Leer el contenido de un puerto es posible en la mayoría de los casos con la instrucción IN. La siguien-te secuencia:

.DEF RegistroPreferido = R16IN RegistroPreferido, MCUCR

lee los bits del puerto MCUCR para el registro de nombre RegistroPreferido. Como algunos puertos tienen bits sin definir y sin usar, estos se leen siempre como ceros.

Más a menudo que la lectura de los 8 bits de un puerto, hay que reaccionar a un determinado estado de un bit dentro de un puerto. En ese caso, no necesita leer todo el puerto solo aislar el bit relevante. Para ello, ciertas instrucciones proporcionan la circunstancia de ejecutar otras instrucciones dependiendo del estado de cierto de bit de algún puerto (ver la sección de salto).

Acceso de lectura-modificación-escritura a los puertos

Es posible activar o desactivar determinados bits de un puerto sin cambiar bits de otro puerto y además sin leer o escribir los otros bits del puerto. Hay dos instrucciones que son SBI (Set Bit I/O) y CBI (Clear Bit I/O). La ejecución es así:

21

Page 22: Ensamblador AVR

.EQU BitActivo=0 ;El bit que se va a cambiarSBI PortB, BitActivo ;Se establece en 1CBI PortB, BitActivo ;Se borra estableciéndolo a cero

Estas dos instrucciones tienen una limitación: sólo se pueden manejar los puertos con una dirección más baja de 0x20, por encima de esta dirección no se pueden acceder los puertos de esta manera. Debido a que el puerto MCUCR de los ejemplos anteriores tiene la dirección hexadecimal $38, los bits del mismo no se pueden establecer o borrar de esta manera. No obstante todos los bits de los puertos que controlan los pines externos del AVR (POTTx, DDRx, PINx), si son accesibles de esta manera.

Asignación de memoria de acceso al puerto

Para programadores exóticos o dispositivos grandes como los Atmega y ATXmega, donde Atmel se quedó sin direcciones de acceso a puertos, éstos se pueden acceder también usando instrucciones de acceso SRAM, como por ejemplo ST y LD. Sólo tiene que sumar 0x20 para la dirección de puerto y ya tiene direcciones de acceso (recordar que las primeras 32 direcciones están asociadas a los registros). Por ejemplo:

.DEF RegistroPreferido = R16LDI ZH,HIGH(PORTB+32)LDI ZL,LOW(PORTB+32)LD RegistroPreferido,Z

Que sólo tiene sentido en ciertos casos, ya que requiere más instrucciones, tiempo de ejecución y líneas de ensamblador, pero es posible. Es también la razón de por que la localización para la primera dirección de SRAM es 0x60 y 0x100 en algunos tipos de AVRs más grandes.

Puertos relevantes de los AVR en detalle

La siguiente tabla contiene los puertos más utilizados del “pequeño” AT90S8515. No figuran todos en esta lista. Algunos de los tipo MEGA y AT90S4434/8535 se omiten. En caso de duda consulte la refe-rencia original.

Componente Nombre de Puerto Registro de Puerto

Acumulador SREG Registro de Estado

Pila SPL/SPH Puntero de Pila

SRAM / Interrupción externas MCUCR Registro de control general MCU

Interrupciones externas GIMSK Registro de máscara de interrupción

GIFR Registro de bandera de interrupción

Temporizador de interrupción TIMSK Registro de máscara de contador de interrupción

TIFR Registro de bandera de contador de interrupción

Temporizador 0 de 8 bits TCCR0 Registro de control de contador/temporizador 0

TCNT0 Temporizador/contador 0

Temporizador 1 de 16 bits TCCR1A Registro de control de contador/temporizador 1 A

TCCR1B Registro de control de contador/temporizador 1 B

TCNT1 Temporizador/contador 1

OCR1A Registro de comparación de salida 1 A

22

Page 23: Ensamblador AVR

OCR1B Registro de comparación de salida 1 B

ICR1L/H Registro de captura de entrada

Temporizador de vigilancia WDTCR Registro de control del temporizador de vigilancia

Acceso EEPROM EEAR Registro de direcciones EEPROM

EEDR Registro de datos EEPROM

EECR Registro de control EEPROM

Interfaz periférico serie SPI SPCR Registro de control periférico serie

SPSR Registro de estado periférico serie

SPDR Registro de datos periférico serie

Comunicación serial UART(Transmisor-Receptor Asíncrono Universal)

UDR Registro de datos UART

USR Registro de estado UART

UCR Registro de control UART

UBRR Registro de velocidad de transmisión (baud rate) UART

Comparador analógico ACSR Control de comparador analógico y Registro de estado

Puertos I/O (entrada/salida) PORTx Registro de puerto de salida

DDRx Registro de dirección de puerto

PINx Registro de puerto de entrada

El registro de estado como puerto más utilizado

Con mucho, el puerto más utilizado es el registro del estado, con sus 8 bits. Por lo general, el acceso a este puerto se hace mediante configuración automática, borrado de bits por la CPU o acumulador, acceso por lectura o ramificación a ciertos bits de este puerto y en algunos casos es posible la manipulación di-recta de estos bits utilizando las instrucciones de ensamblador Sex o Clx donde x señala el bit. La mayo-ría de estos bits están activados o desactivados por el acumulador para operaciones de test, comparación o cálculo.

Los bits que más se utilizan son:

-Z: Si está a 1, la instrucción anterior tuvo como resultado cero.-C: Si está a 1, la instrucción anterior causó un acarreo del bit más significativo.

La siguiente lista tiene todas las instrucciones de ensamblador para activar o desactivar los bits de estado en función del resultado de la ejecución de la instrucción anterior:

Bit Cálculo Lógico Comparación Bits Desplazamiento Otro

Z ADD, ADC, ADIW, DEC, INC, SUB, SUBI, SBC, SBCI, SBIW

AND, ANDI, OR, ORI, EOR, COM, NEG, SBR, CBR

CP, CPC, CPIBCLR Z, BSET Z, CLZ, SEZ, TST

ASR, LSL, LSR, ROL, ROR CLR

C ADD, ADC, ADIW, SUB, SUBI, SBC, SBCI, SBIW

COM, NEG CP, CPC, CPIBCLR C, BSET C, CLC, SEC

ASR, LSL, LSR, ROL, ROR

-

N ADD, ADC, ADIW, DEC, INC, SUB, SUBI, SBC, SBCI, SBIW

AND, ANDI, OR, ORI, EOR, COM, NEG, SBR, CBR

CP, CPC, CPIBCLR N, BSET N, CLN, SEN, TST

ASR, LSL, LSR, ROL, ROR CLR

23

Page 24: Ensamblador AVR

V ADD, ADC, ADIW, DEC, INC, SUB, SUBI, SBC, SBCI, SBIW

AND, ANDI, OR, ORI, EOR, COM, NEG, SBR, CBR

CP, CPC, CPIBCLR V, BSET V, CLV, SEV, TST

ASR, LSL, LSR, ROL, ROR CLR

S SBIW - - BCLR S, BSET S, CLS, SES

- -

H ADD, ADC, SUB, SUBI, SBC, SBCI NEG CP, CPC, CPI

BCLR H, BSET H, CLH, SEH

- -

T - - - BCLR T, BSET T, BST, CLT, SET

- -

I - - - BCLR I, BSET I, CLI, SEI

- RETI

Los detalles de los puertos más comunes se muestran en una tabla adicional (anexo).

SRAM

Casi todos los dispositivos AVR disponen de memoria RAM estática -SRAM- (algunos pocos antiguos no la llevan). Sólo los programas muy simples en ensamblador, pueden evitar el uso de este espacio de memoria, por tener toda la información necesaria en los registros. Si te quedas sin registros debes ser capaz de programar la SRAM para utilizar más espacio.

¿Qué es la SRAM?La SRAM es una memoria que no es directamente accesible por la CPU (unidad aritmética lógica -ALU- a veces llamada acumulador) como si son los registros. Si se accede a estos espacios de memoria hay que utilizar un registro como almacenamiento provisional.En el ejemplo, se muestra como un valor de SRAM se copia en el registro R2 (1ª instrucción), se hace un cálculo con un valor en el registro R3(2ª ins-trucción) y después, el resultado se

vuelve a escribir en la misma ubicación de la SRAM (3ª instrucción, no mostrada aquí).

Por lo cuál, es claro que las operaciones con valores almacenados en la SRAM son más lentas de realizar que utilizando registros. Sin embargo, incluso el más pequeño de los AVR tienen 128 bytes de SRAM disponibles, mucho más de lo que pueden contener los 32 registros.

Los AVR desde el AT90S8515 en adelante, ofrecen la capacidad de conectar RAM externa adicional, ampliando la interna 512 bytes. Desde el punto de vista del ensamblador, a la SRAM externa se accede como SRAM interna. No hay instrucciones adicionales.

¿Para qué fines puedo usar SRAM?

Además de almacenamiento simple de los valores, la SRAM ofrece oportunidades adicionales para su uso. No sólo es posible el acceso con direcciones fijas, sino también el uso de punteros, por lo que se puede

24

Page 25: Ensamblador AVR

programar el acceso flotante a la SRAM. Así, se pueden crear búfferes de anillo para almacenamiento provisional de valores o tablas (variables) de cálculo. Esto no es muy frecuente con los registros, porque además de ser pocos, se prefieren para el acceso fijo.

Aún más habitual es el acceso mediante un desplazamiento de una dirección fija a partir de un registro de puntero. En este caso, una dirección fija se almacena en un registro puntero, un valor constante se suma a esta dirección y la lectura/escritura se hace con un desplazamiento de esa dirección. Este tipo de acceso a tablas es mucho más eficaz.

Pero el uso más relevante de la SRAM son las llamadas a la pila. Se pueden poner valores (variables) en la pila, ya sea el contenido de un registro que es temporalmente necesario para otro fin, ya sea una dirección de retorno antes de llamar a una subrutina o la dirección de retorno antes de atender una interrupción.

¿Cómo usar SRAM?Direccionamiento directo

Para copiar un valor a una posición de memoria de la SRAM tiene que definir la dirección. Las direccio-nes SRAM que puede utilizar abarcan desde la dirección de inicio (muy a menudo 0x0060 en pequeños AVRs, 0x0100 en Atmega más grandes) hasta el final físico de la SRAM (en el AT90S8515 el lugar accesible de la SRAM interna más alto es 0x025F, ver la hoja de datos de cada AVR para los detalles).

Con la instrucción:

STS 0x0060, R1

el contenido del registro R1 se copia en la primera dirección de memoria 0x0060. Con:

LDS R1, 0x0060

el contenido de la SRAM en la dirección 0x0060 se copia en el registro. Este es el acceso directo con una dirección que tiene que ser definida por el programador.

Los símbolos definidos en el archivo de inclusión *def.inc, SRAM_START y RAMEND, permiten colocar las variables en el espacio de SRAM, por lo que es mejor utilizar estas definiciones para acceder al quinceavo byte de memoria, así:

LDS R1,SRAM_START+15

Los nombres simbólicos se puede utilizar para evitar la manipulación de direcciones fijas, que requieren mucho trabajo, si luego se quiere cambiar la estructura de los datos en la SRAM. Estos nombres son más fáciles de manejar que los números hexadecimales, por lo que ocuparse de dar un nombre como:

.EQU MyPreferredStorageCell = SRAM_STARTSTS MyPreferredStorageCell, R1

Si, el nombre es más largo, pero más fácil de recordar. Use cualquier nombre que crea conveniente.

Puntero a direcciones

Otro tipo de acceso a la SRAM es a través de punteros. Se necesitan dos registros para este fin que contengan la dirección de 16 bits. Como hemos visto en la división de registros de puntero, los registros de puntero son los pares de registros X (XH:XL, R27:R26), Y (YH:YL, R29:R28) y Z (ZH:ZL, R31:R30), que permiten el acceso directo a la ubicación que elija (ej.: ST X, R1). O después de decrementar la dirección con por ej.: ST -X, R1 o después de incrementarla (ST X+, R1). Un acceso completo a una hilera de tres celdas de memoria luciría así:

25

Page 26: Ensamblador AVR

.EQU MyPreferredStorageCell = SRAM_START

.DEF MyPreferredRegister = R1

.DEF AnotherRegister = R2

.DEF AndYetAnotherRegister = R3LDI XH, HIGH(MyPreferredStorageCell)LDI XL, LOW(MyPreferredStorageCell)LD MyPreferredRegister, X+LD AnotherRegister, X+LD AndYetAnotherRegister, X

Con los punteros es fácil operar, tanto como en otros idiomas a parte del ensamblador, que dicen ser más fáciles de aprender.

Puntero con desplazamiento

La tercera construcción es un poco más exótica y sólo programadores experimentados deben usar esto en ciertos casos. Vamos a suponer que en nuestro programa a menudo se necesita acceder a tres lugares de SRAM consecutivos. Vamos a suponer también que aún disponemos de un par de registros de puntero de repuesto, por lo que puede darse el lujo para el siguiente propósito. Si queremos utilizar las instrucciones ST / LD siempre tenemos que cambiar el puntero si accedemos a otra ubicación de las tres del ejemplo anterior.

Para evitar esto (y para confundir a los principiantes) se inventó el acceso con desplazamiento. Durante este acceso el valor del registro no se modifica. La dirección se calcula temporalmente sumando un desplazamiento fijo. En el ejemplo anterior el acceso a la ubicación 0x0062 se vería de la siguiente manera. Primero, el registro de punteo se ajusta a nuestra ubicación central SRAM_START:

.EQU MyPreferredStorageCell = SRAM_START

.DEF MyPreferredRegister = R1LDI YH, HIGH(MyPreferredStorageCell)LDI YL, LOW(MyPreferredStorageCell)

En algún lugar más adelante del programa me gustaría escribir en la celda 2 por encima de SRAM_START:

STD Y+2, MyPreferredRegister

La correspondiente instrucción para la lectura SRAM con un desplazamiento también es posible:

LDD MyPreferredRegister, Y+2

Tenga en cuenta que el 2 no se suma realmente a Y, sólo temporalmente durante la ejecución de esta instrucción. Para “confundir” más, esto sólo se puede hacer con los pares de registro Z e Y, no se puede hacer con el puntero X. De cada 100 casos, el uso de esta técnica se muestra más eficaz en un solo caso, así que no importa si no entiende esto en detalle. Es sólo para expertos y necesario sólo en algunos casos.

El uso de SRAM como pila

El uso más común y relevante de la SRAM es como pila. La pila es como una torre de bloques. Cada bloque que se añade va en la parte superior de la torre, cada llamada a un valor elimina el bloque más alto de la torre. La eliminación de bloques de la base o de cualquier porción baja de la torre es demasiado complicado y desestructuraría el conjunto de la pila, por lo que se recomienda no probar nunca esto. Esta estructura de pila se denomina LIFO (Last-In-First-Out, el último en entrar es el primero en salir).

Definición de SRAM como pila

Para usar la SRAM como pila se requiere primero el establecimiento del puntero de pila. Éste es de 16 bits y es accesible como puerto. El doble registro se denomina SPH:SPL. SPH contiene el octeto más signifi-cativo y SPL, el menos significativo.

26

Page 27: Ensamblador AVR

Esto sólo es válido, si el tipo de AVR tiene más de 256 bytes de SRAM. Si no, SPH no es necesario, no está definido, y no se debe ni se puede utilizar. En los siguientes ejemplos asumiremos que tenemos una SRAM con más de 256 bytes.

Para construir la pila, el puntero de pila se carga con la dirección disponible más alta de la SRAM. (En nuestro caso la torre crece hacia abajo, hacia las direcciones bajas, sólo por razones históricas y para ¡“confundir” a los principiantes!).

.DEF RegistroPreferido = R16LDI RegistroPreferido, HIGH(RAMEND) ;byte altoOUT SPH,RegistroPreferido ;del puntero de pilaLDI RegistroPreferido, LOW(RAMEND) ;byte bajoOUT SPL,RegistroPreferido ;del puntero de pila

Por supuesto, el valor RAMEND es específico para el tipo de procesador y está definido en el archivo de inclusión. En el archivo 8515def.inc la línea:

.equ RAMEND =$25F ;última dirección del chip

El archivo 8515def.inc se incluye en la directiva de ensamblador:

.INCLUDE "C:\algúnlugar\8515def.inc"

al principio de nuestro código fuente ensamblador.

Por lo tanto, se define la pila ahora, y no tiene que preocuparse más por el puntero de pila debido a que las manipulaciones de este indicador son automáticas en su mayoría.

El uso de la pila

El contenido de los registros se ponen en la pila de la siguiente manera:

PUSH RegistroPreferido ;Pone el valor en la cima de la pila

En caso de que el valor deje de tener interés, como el puntero de pila se decrementa después de ponerlo, no tiene que preocuparse. Si necesitamos el contenido de nuevo, simplemente añadimos la siguiente instrucción:

POP RegistroPreferido ;Leer de nuevo el valor de la cima de la pila

Con POP acaba de obtener el valor que fue puesto por última vez en el tope de la pila. Poner y quitar registros tiene sentido si:

– El contenido es necesario algunas líneas de código más tarde,– todos los registros están en uso, y si– no existe otra opción de almacenar ese valor en otra parte.

Si estas condiciones no se dan, el uso de la pila para guardar los registros es inútil y una pérdida de tiempo de procesador.

Donde más sentido tiene el uso de la pila es con las subrutinas, donde después de ejecutarlas se tiene que volver a la ubicación del programa que llamó a la subrutina. Este pone la dirección de retorno (el valor actual del contador de programa) en la pila y salta temporalmente a la subrutina. Cuando acaba ésta, se coje la dirección de retorno de la pila y se carga de nuevo en el contador de programa. La ejecución del programa continúa exactamente en la instrucción siguiente en la que estaba el programa cuando ocurrió la llamada:

RCALL algo ;Saltar a la etiqueta "algo"[…] aquí después se continuará con el programa.

Aquí el salto a la etiqueta "algo" en algún lugar del código del programa,

algo: ;esta es la dirección del salto.[…] aquí hacemos algo

27

Page 28: Ensamblador AVR

[…] termina, y se vuelve a desde donde se hizo la llamadaRET

Durante la ejecución de la instrucción rcall el contador de programa se incrementa. Una dirección de 16 bits se inserta en la pila en dos pasos (2 PUSH), el LSB y el MSB. Al llegar a la instrucción RET, el contenido del contador de programa se vuelve a cargar con estos dos bytes (2 POP) y desde aquí continúa la eje-cución. No tiene que preocuparse por la dirección de la pila desde donde se carga el contador, ya que esta dirección se genera automáticamente, incluso si se llama a otra subrutina dentro de la anterior sub-rutina, la pila funciona bien. Se ponen dos direcciones de retorno en la parte superior de la pila, la de la subrutina anidada encima de la que llama, que cuando termina su ejecución retorna a la subrutina que la llamó y ésta a su vez hace lo propio. Mientras haya suficiente memoria SRAM, todo está bien.

El servicio de interrupciones de hardware no es posible sin la pila. Una interrupción, como su nombre indica, interrumpe la ejecución normal del programa. Después de la ejecución de la rutina de servicio a la interrupción, el programa debe volver justo a donde estaba cuando se produjo la interrupción. Esto no sería posible si no se pudiera almacenar la dirección de retorno en la pila.

Las enormes ventajas de tener una pila para poder atender las interrupciones son la razón por la que hasta el más mínimo AVR sin necesidad de SRAM, tenga el menos una pequeña pila en hardware.

Errores comunes en la operación de pila

Los principiantes, como es normal, cometen una gran cantidad de errores cuando comienzan a apren-der a utilizar la pila.

No es muy inteligente el uso de la pila sin establecer o definir primero el puntero de pila. Debido a que este puntero se establece en cero al inicio del programa, apunta a la dirección 0x0000 que es donde se encuentra el registro R0. Poner un byte (PUSH) escribiendo en dicho registro, sobreescribiría su con-tenido anterior. Un PUSH adicional escribiría en 0xFFFF que es una posición indefinida si no tiene memoria SRAM externa. RCALL y RET devolverán direcciones extrañas en la memoria de programa y puede estar seguro de que no habrá ninguna advertencia como la aparición de una ventana diciendo algo así como “acceso ilegal a dirección de memoria xxxx”.

Otros errores comunes son olvidarse de sacar de la pila un valor puesto anteriormente o intentar sacar un valor sin haberlo puesto antes. También puede suceder en algún caso, el desbordamiento de pila por debajo de la primera dirección SRAM. Esto se da cuando se produce una llamada recursiva sin fin. Después de alcanzar la dirección más baja de SRAM los siguientes PUSH escriben en los puertos (desde 0x005F hasta 0x0020), y los siguientes en los registros (de 0x001F a 0x0000). Si esto continúa, cosas divertidas e impredecibles pueden suceder en el chip. Evite este error, ¡incluso puede destruir su hardware externo!

Salto y Ramificación

Aquí hablaremos de todas las instrucciones que controlan la ejecución secuencial de un programa. Se inicia con la secuencia de arranque del procesador, continúa con saltos, interrupciones, etc.

Control secuencial de la ejecución del programa¿Qué sucede durante un reinicio?

Cuando al AVR se le suministra la tensión de alimentación y el procesador comienza su labor, se activa una secuencia de reinicio. Los puertos se establecen a sus valores iniciales, tal como se define en la hoja de datos del dispositivo y el contador de programa se establece a cero y en esta dirección

28

Page 29: Ensamblador AVR

comienza la ejecución del programa, donde debemos tener nuestra primera palabra de código. Pero no sólo durante el encendido se activa esta dirección:

– Si se produce un reset externo en el pin de reset el dispositivo ejecuta un reinicio.– Si llega a su valor máximo el contador del perro guardián (watchdog) también se ejecuta un

reinicio. El temporizador de vigilancia es un reloj interno que debe ser reseteado de vez en cuando por el programa, de lo contrario, se reinicia el procesador.

– Usted puede llamar a reinicio por un salto directo a esa dirección (véase la sección salto -jump- más adelante).

El tercer caso no es un reset real, porque no se ejecuta el reajuste automático de valores de registros y puertos a un valor por defecto bien definido. Por lo tanto, nos olvidamos de esto por el momento. El ajuste del watchdog a cero para evitar un reinicio requiere la ejecución de la instrucción:

WDR

Después de la ejecución de un reset, con el establecimiento de registros y puertos a los valores por defecto, el código en la dirección 0000 (tipo palabra) comienza a ejecutarse en el procesador. El contador de programa se incrementa en uno y la siguiente palabra de código está ya en el buffer de lectura. Si la instrucción ejecutada no requiere un salto se ejecuta la siguiente inmediatamente. Esta es la razón por la que los AVR son tan rápidos, cada ciclo dde reloj ejecuta una instrucción si no se producen saltos.

La primera instrucción de un archivo ejecutable se encuentra siempre en la dirección 0000. Para indicar al compilador (ensamblador del programa) que nuestro código fuente empieza aquí y ahora,puede colocarse al principio una directiva especial:

.CSEG

.ORG 0000

La primera directiva. CSEG, permite al compilador cambiar su salida a una sección de código, es decir define el inicio de un segmento de código. Todo lo siguiente se traduce como código y se escribe en la sección del procesador de la memoria de programa. Otro segmento destino sería la sección EEPROM del chip, donde también se pueden escribir bytes o palabras.

.ESEG

El tercer segmento es la sección de SRAM.

.DSEG

Excepto en la EEPROM, donde el contenido es realmente lo que está pasando en la EEPROM durante la programación del chip, el contenido del segmento DSEG no está programado para el chip. No tiene posibilidad de grabar contenido en SRAM. Así que DSEG. sólo se utiliza para el cálculo de la etiqueta correcta durante el proceso de ensamblado. Un ejemplo:

.DSEG ;Las siguientes son definiciones de etiqueta dentro del segmento SRAMPrimeraVariableByte:.BYTE 1 ;El puntero DSEG se mueve hacia arriba un byteSegundaVariablePalabra:.BYTE 2 ;El puntero DSEG se mueve hacia arriba dos bytesTerceraVariableBuffer:.BYTE 32 ;El puntero DSEG se mueve hacia arriba 32 bytes

29

Page 30: Ensamblador AVR

Así, sólo se definen tres etiquetas en el código, no se produce contenido.

La directiva ORG en el segmento de código, pasa por encima de la primera palabra de código y manipula la dirección donde debe ir, es decir establece el inicio (ORiGin) del programa.

En los programas diseñados para que de todas formas empiecen en la dirección 0x0000, las directivas CSEG/ORG son triviales y puede pasarlas por alto sin generar error. Podríamos comenzar en 0x0100, pero no tiene ningún sentido real cuando el procesador comienza su ejecución en 0000. Si desea colocar una tabla exactamente en un determinado lugar del segmento de código, puede utilizar .ORG, pero tenga cuidado con lo siguiente: Con .ORG solo se da el salto hacia delante, nunca hacia atrás. Por lo que hay que tener cuidado también, ya que el salto forzado por .ORG en espacio de memoria flash donde se ubica el código corriente, está lleno de la instrucción 0xFFFF. Esta instrucción no hace nada, solo ir a la siguiente instrucción, asi que asegurese de que su ejecución no salte en medio de tal espacio indefinido.

Si en el principio de su sección de código desea establecer una señal clara, después de las definiciones con las directivas .DEF y .EQU, puede uasr la secuencia CSEG/ORG como una señal para usted, aunque no hubiera sido necesario hacerlo.

Como la primera palabra de código está siempre en la dirección cero, a este lugar se le llama también el vector de reset. Tras éste, las siguientes posiciones en el espacio de memoria, las direcciones 0x0001, 0x0002, etc, son vectores de interrupción. Estas son las posiciones a las que salta la ejecución si se produce una interrupción interna o externa. Estos vectores son específicos para cada tipo de procesador y dependen del hardware interno (véase más adelante). La instrucción para reaccionar ante una interrupción dada debe ser colocada en el vector adecuado. Si se utilizan interrupciones, el primer código en el vector de reset, debe ser una instrucción de salto, para sortear estos otros vectores. Cada vector de interrupción que se planifique que esté habilitado, debe tener una instrucción de salto a la rutina de servicio de interrupción respectiva. Si el vector no se utiliza, lo más indicado es poner una instrucción RETI (Retorno de interrupción) ficticia. La secuencia típica del programa al principio es como sigue:

.CSEG

.ORG 0000RJMP Start ;el vector de resetRJMP IntServRout1 ;la rutina de servicio de la 1ª interrupciónRETI ;instrucción ficitcia de interrupción sin usarRJMP IntServRout3 ;la rutina de servicio para la 3ª interrupción

[...] aquí ponemos las otras instrucciones del vector de interrupción[...] y aquí es un buen lugar para las rutinas de servicio de interrupciónIntServRout1:

[...] Código de la rutina de servicio de la 1ª interrupciónRETI ;Final de la 1ª rutina de servicio

IntServRout2: [...] Código de la rutina de servicio de la 3ª interrupción

RETI ;Final de la 2ª rutina de servicio[...] otro códigoStart: ;Aquí el inicio del programa[...] Aquí el programa principal

La instrucción “RJMP Start” resulta en un salto a la etiquete Start:, que se encuentra algunas líneas debajo. Recuerde que las etiquetas terminan siempre con un “ : ”. Las etiquetas que no cumplan esta condición no se tienen en cuenta, dando un mensaje de error "Undefined label" (etiqueta no definida) y se interrumpe la compilación.

Ejecución lineal del programa y bifurcaciones

30

Page 31: Ensamblador AVR

La ejecución del programa es siempre lineal, si nada cambia la ejecución secuencial. Estos cambios pueden producirse por la ejecución de una interrupción o de instrucciones de ramificación.

Ramificación

La ramificación se suele producir en función de alguna condición llamada bifurcación condicional. Como ejemplo vamos a suponer que queremos construir un contador de 32 bits utilizando los registros R1 a R4. El byte menos significativo en R1 se incrementa en uno. Si el registro se desborda durante la operación (255 + 1 = 0), incrementamos R2. Si se desborda R2, incrementamos R3 y así sucesivamente.

El incremento en uno se hace con la instrucción INC. Si ocurre un desbordamiento durante la ejecución de INC R1, el bit cero en el registro de estado se establece a 1 (el resultado de la operación es cero). El bit de acarreo en el registro de estado se establece a 1 cuando hay un desbordamiento, no cambia cuando se ejecuta INC. Se trata de no confundir al principiante pero el acarreo se puede utilizar para otros fines. Si no hay desbordamiento podemos salir de la secuencia de conteo.

Si el bit cero está establecido a 1, se debe ejecutar el incremento del registro superior siguiente. La instrucción de ramificación que se utiliza en este caso es BRNE (Branch if Not Equal). La secuencia de conteo del contador de 32 bits debería tener este aspecto:

INC R1 ;incrementa el contenido del registro R1BRNE GoOn32 ;si no es cero, ir a la rama GoOn32:INC R2 ;incrementa el contenido del registro R2BRNE GoOn32INC R3BRNE GoOn32INC R4

GoOn32:

Esto es todo. La condición opuesta a BRNE es BREQ (Branch Equal).

Cuál de los bits de estado, también llamados banderas del procesador, cambian durante la ejecución de una instrucción, se muestra en las tablas de códigos de instrucciones, consulte la lista de instrucciones. Al igual que el bit cero se podrían utilizar otros bits de estado, como por ejemplo,

Etiquetas BRCC/BRCS; bandera de acarreo 0 (BRCC) o 1 (BRCS)Etiqueta BRSH; igual o mayorEtiqueta BRLO; menorEtiqueta BRMI; menosEtiqueta BRPL; másEtiqueta BRGE; Mayor o igual (con bit de signo)Etiqueta BRLT; menor (con bit de signo)Etiquetas BRHC/BRHS; bandera medio desbordamiento, 0 o 1Etiquetas BRTC/BRTS; bit T 0 o 1Etiquetas BRVC/BRVS; bandera de complemento a dos, 0 o 1Etiquetas BRIE/BRID; interrupción activada o desactivada

para reaccionar a las diferentes condiciones. La ramificación siempre se produce si se cumple la condición. La mayoría de estas instrucciones se utilizan muy poco. Para el principiante, con el bit cero y el de acarreo son suficientes.

El tiempo durante la ejecución del programa

Como se ja mencionado anteriormente, el tiempo necesario para ejecutar una instrucción es igual a un ciclo de reloj del procesador. Si el procesador corre a una frecuencia de reloj de 4 Mhz, una

31

Page 32: Ensamblador AVR

instrucción requiere ¼ de microsegundo ó 250 ns. A 10 MHz requeriría 100 ns. El tiempo requerido es tan exacto como lo sea el reloj interno o externo o Xtal. Si necesitamos el tiempo exacto, un AVR es la solución. Tenga en cuenta que hay algunas instrucciones que requieren dos o más ciclos como las instrucciones de ramificación (si se produce la ramificación) o la lectura/escritura secuencial de SRAM. Consulte la tabla de instrucciones para los detalles.

Para poder definir el momento exacto, debe haber una oportunidad de no hacer nada más que retrasar la ejecución del programa. Se pueden utilizar otras instrucciones que no hagan nada, pero es mejor utilizar la instrucción NOP (no-operation). Esta es la instrucción más inoperante:

NOP

Esta instrucción no hace más que perder tiempo de procesador. A 4 Mhz, necesitamos justo cuatro instrucciones NOP para consumir 1 μs. No hay otros ocultos significados aquí en la instrucción NOP. Para un generador de señal de 1 Khz no es necesario añadir 4.000 instrucciones NOP a nuestro código fuente. En este caso se utilizaría un contador de software y algunas instrucciones de ramificación con los que se construiría un bucle que se ejecuta un determinado número de veces y hacen el retardo con exactitud. Un contador podría ser un registro de 8 bits que se decrementa con la instrucción DEC, así por ejemplo, de esta manera:

CLR R1 ;un ciclo de relojCount:

DEC R1 ;un ciclo de relojBRNE Count ;dos para la bifurcación, uno por la no bifurcación

Esta secuencia consume (1) + (255*2) + (1*3) = 514 ciclos de reloj ó 128,5 μs a 4 MHz.

También se puede utilizar un contador de 16 bits para hacer un retardo exacto de esta forma:

LDI ZH,HIGH(65535) ;un ciclo de relojLDI ZL,LOW(65535) ;un ciclo de reloj

Count:SBIW ZL,1 ;dos ciclos de relojBRNE Count ;dos para la bifurcación, uno por la no bifurcación

Esta secuencia consume (1+1) + (65534*4) + (1*3) = 262,141 ciclos de reloj ó 65.535,25 μs a 4 Mhz.

Si se utilizan más registros para construir contadores anidados se puede llegar a cualquier retraso, y éste es tan exacto como lo sea la fuente de reloj, incluso sin un temporizador de hardware.

Las macros y la ejecución del programa

A menudo se tienen que escribir secuencias de código idénticas o similares en diferentes ocasiones y lugares en el código fuente. Si quiere puede escribirlo una sola vez utilizando una macro para evitar el cansancio de escribir la misma secuencia varias veces. Las macros son secuencias de código, que una vez diseñadas y comprobado su correcto funcionamiento, se insertan en el código por el nombre de la macro.

Como ejemplo vamos a suponer que tenemos que retrasar varias veces el tiempo de ejecución del programa en 1 μs con un reloj de 4 Mhz. En algún lugar del fichero fuente se define una macro:

.MACRO Delay1NOPNOPNOPNOP.ENDMACRO

32

Page 33: Ensamblador AVR

Esta definición de la macro aún no produce ningún código, no hace nada. El código sólo se producirá si se llama a esa macro por su nombre:

[...] en algún lugar del código fuenteDelay1[...] el código va aquí

Esto hace que cuatro instrucciones NOP sean insertadas en el código en ese lugar. Adicionales "DELAY1" insertaran otras cuatro instrucciones NOP.

Si la macro tiene secuencias muy largas de código o si tiene poco espacio de almacenamiento de código, debe evitar el uso de macros y utilizar subrutinas en su lugar. Al llamar a una macro por su nombre, puede agregar algunos parámetros para manipular el código producido, pero por ahora esto es lo que debe saber como principiante acerca de las macros.

Subrutinas

Al contrario que las macros las subrutinas hacen ahorrar espacio de almacenamiento de programa. La secuencia se almacena una sola vez en el código y luego se puede llamar desde cualquier parte del mismo. Para asegurar la ejecución continua de la secuencia siguiente después de ejecutar la subrutina, se necesita volver a la secuencia llamadora. Para un retraso de 10 ciclos se necesitaría por ejemplo esta subrutina:

Delay10: ;la llamada de la subrutina requiere algunos ciclosNOP ;retraso de un cicloNOP ;retraso de un cicloNOP ;retraso de un cicloRET ;Volver de la llamada

Para que se pueda saltar a ellas, las subrutinas siempre empiezan con una etiqueta. En el ejemplo la etiqueta es “Delay10:”. Le sigue tres instrucciones NOP y una instrucción RET. Si se cuentan los ciclos se encuentran 7 (3 para las NOP y 4 para la RET). Las 3 que faltan son para llamar a la subrutina:

[...]:RCALL Delay10[...] más adelante en el código fuente

Rcall es una llamada relativa. La llamada se codifica como salto relativo. La distancia relativa desde la rutina llamadora hasta la subrutina es calculada por el compilador. La instrucción RET salta de nuevo a la rutina llamadora. Tenga en cuenta que antes de hacer llamadas a subrutinas debe esta-blecer el puntero de pila, debido a que la dirección de retorno debe ser puesta en la cima de la pila durante la instrucción RCALL.

Si desea ir directamente a alguna otra parte del código, tiene que utilizar la instrucción de salto:

[...] en algún lugar del código fuenteRJMP Delay10

Return:[...] más adelante en el código fuente

Tenga en cuenta que RJMP también es una instrucción de salto relativo con la distancia limitada. Sólo los AVRs Atmega tienen una instrucción JMP que permite saltos en el espacio completo de memoria flash, pero esta instrucción requiere dos palabras y más tiempo de instrucción que RJMP por lo que es mejor evitarla si es posible.

La rutina que saltó no puede utilizar la instrucción RET en este caso, porque RJMP no coloca la dirección actual de ejecución en la pila. Para volver de vuelta al lugar de la llamada se requiere

33

Page 34: Ensamblador AVR

agregar otra etiqueta y la rutina llamada salta de nuevo a esta etiqueta. Este salto no es como llamar a una subrutina porque no se la puede llamar desde distintas ubicaciones del código.

RCALL y RJMP son bifurcaciones incondicionales. Para ir a otro lugar de forma condicionada tiene que combinarlas con otras. Una llamada a subrutina condicionada, sde puede hacer con las siguientes (confusas) instrucciones. Si quiere llamar a una subrutina en función de cierto bit de un registro puede utilizar la siguiente secuencia:

SBRC R1,7 ;Saltar la siguiente instrucción si el bit 7 en el registro 1 es 0RCALL UpLabel ;Llamada a subrutina

SBRC es Skip siguiente inst. si Bit x en Registro es Clear (=Cero). La instrucción RCALL a “UpLabel” solo se ejecuta si el bit 7en el registro R1 es 1, ya que la siguiente instrucción a SBRC se saltaría si fuese 0. Si se quisiera que sea al contrario, llamar a la subrutina en caso de que el bit correspondiente fuese 0, se utilizaría la instrucción SBRS. La siguiente instrucción a SBRS/SBRC puede ser de una palabra o de doble palabra. En todo caso, el procesador sabe el salto que tiene que hacer, pero tenga en cuenta que los tiempos de ejecución son diferentes. Para saltar más allá de la siguiente instrucción, SBRS/SBRC no se pueden utilizar.

Si tuviera que saltar una instrucción para el caso de dos registros que tuvieran el mismo valor se puede utilizar la siguiente exótica instrucción (que se utiliza raras veces):

CPSE R1,R2 ;Comparar R1 y R2 y saltar la siguiente instrucción si son igualesRCALL AlgunaSubrutina ;Llamada a AlgunaSubrutina

Si por ejemplo tuviera que saltar la instrucción siguiente en función de determinado bit de un puerto, se utilizarían las instrucciones SBIC y SBIS (Skip si el Bit I/O es Clear -o Set-), así:

SBIC PINB,0 ;Saltar la siguiente instrucción si el bit 0 en el puerto B es 0RJMP ATarget ;Saltar a la etiqueta Atarget

La instrucción RJMP sólo se ejecuta si el bit 0 en el puerto B está alto. Esto es algo confuso para el principiante. El acceso a los bits de puerto se limita a la mitad inferior de los puertos, los 32 puertos superiores no se pueden utilizar aquí.

Ahora, a modo de ejemplo, otra exótica aplicación para expertos. Supongamos que tenemos un interruptor de bits con 4 interruptores conectados al puerto B. Dependiendo del estado de esos 4 bits habría que ir a 16 lugares diferentes en el código. Entonces tenemos que leer el puerto y utilizar varias instrucciones de bifurcación para ir al lugar adecuado. Como alternativa, se puede escribir una tabla que contenga las 16 direcciones así:

MyTab:RJMP Routine1RJMP Routine2[...]RJMP Routine16

En el código copiamos la dirección de la tabla para el registro de puntero Z:

LDI ZH,HIGH(MyTab)LDI ZL,LOW(MyTab)

y sumamos el estado actual del puerto B (en R16) a esta dirección

ADD ZL,R16BRCC NoOverflowINC ZH

NoOverflow:

34

Page 35: Ensamblador AVR

Ahora podemos ir a esta ubicación en la tabla, ya sea para llamar a una subrutina:

ICALL ;llama a la subrutina que se encuentra en la dirección Z

o como un salto sin camino de regreso:

IJMP ;ir directamente a la dirección en Z

El procesador carga el contenido del par Z en el contador de programa y continúa la operación allí. ¡Mejor que bifurcar una y otra vez!

Interrupciones y ejecución del programa

A menudo tenemos que reaccionar a condiciones de hardware u otros eventos. Un ejemplo es un cambio en un pin de entrada. Usted puede haber programado un bucle que pregunta si se produce un cambio en un pin para tomar una decisión. Este método se llama polling (sondeo), que se aseme-ja a una abeja dando vueltas en círculo buscando nuevas flores. Si no hay otras cosas que hacer y el tiempo de respuesta no importa, se puede dedicar el procesador a esto. Si tiene que detectar pulsos de menos de 1 μs, este método no sirve. En este caso, se necesita programar una interrupción.

Una interrupción se activa por ciertas condiciones de hardware. Todas las interrupciones de hardware están deshabilitadas de forma predeterminada en el reset, por lo que hay que activar la condición en primer lugar. Se establecen primero los bits de puerto correspondientes que permitan habilitar las interrupciones. El procesador tiene un bit en su registro de estado que le permite habilitar el respon-der a las interrupciones, la bandera de habilitación de interrupción. La activación general de respues-ta a las interrupciones requiere de la instrucción:

SEI ;Set Int Enable Bit

Después, para estar habilitadas, cada interrupción por separado requiere además la manipulación en el puerto correspondiente.

Si se produce la condición que lleva a la interrupción, por ejemplo, un cambio en un bit de un puerto, el procesador pone en la pila (habiendo habilitado primero el puntero de pila) el valor actual del con-tador de programa. Sin esto, el procesador no sería capaz de volver al lugar donde se produjo la in-terrupción (que puede ser en cualquier momento y en cualquier lugar durante la ejecución del pro-grama). Después, el procesador salta a la ubicación predefinida, el vector de interrupción, y ejecuta la instrucción. Por lo general, ésta suele ser una instrucción de salto (JUMP) a la rutina de servicio de la interrupción, que se encuentra en algún lugar del código. El vector de interrupción es una localiza-ción específica del procesador y va en función de los componentes de hardware y de la condición que conduce a la interrupción. Cuántos más componentes de hardware y más condiciones, más vec-tores. Los diferentes vectores para algunos tipos de AVR antiguos se enumeran en la tabla siguiente. El primer vector no es de interrupción, es el de reset que no realiza ninguna operación con la pila.

NombreDirecc. del vector de interrupción

Provocada por2313 2323 8515

RESET 0000 0000 0000 Hardware Reset, Power-On-Reset, Watchdog Reset

INT0 0001 0001 0001 Cambio de nivel en el pin externo INT0

INT1 0002 - 0002 Cambio de nivel en el pin externo INT1

TIMER1CAPT 0003 - 0003 Captura de eventos en Timer/Counter 1

TIMER1 COMPA - - 0004 Timer/Counter 1 = Compare value A

TIMER1 COMPB - - 0005 Timer/Counter 1 = Compare value B

TIMER1 COMP1 0004 - - Timer/Counter 1 = Compare value 1

35

Page 36: Ensamblador AVR

TIMER1 OVF 0005 - 0006 Timer/Counter 1 Overflow

TIMER0 OVF 0006 0002 0007 Timer/Counter 0 Overflow

SPI STC - - 0008 Transmisión serie completa

UART TX 0007 - 0009 UART Disponible un carácter en el búfer

UART UDRE 0008 - 000A UART transmisor corrió vacío

UART TX 0009 - 000B UART Todo enviado

ANA_COMP - - 000C Comparador Analógico

Hay que tener en cuenta que la capacidad de reaccionar ante los acontecimientos es muy diferente para los diferentes AVRs. Las direcciones son secuenciales, pero las mismas para los diferentes tipos. Consulte la hoja de datos para cada tipo de AVR.

Cuanto más alto sea un vector en la lista, mayor es su prioridad. Si dos o más componentes tienen una condición de interrupción en espera al mismo tiempo, el vector con la dirección más alta gana. La interrupción inferior tiene que esperar hasta que la interrupción superior este servida. La deshabili-tación de interrupciones bajas durante la ejecución de la rutina de servicio de una primera interrup-ción se realiza con una bandera del procesador que la rutina de servicio debe volver a activar una vez que acabe. Para volver a establecer el bit de estado hay dos formas. La rutina de servicio puede terminar con la instrucción:

RETI

Este retorno de la rutina restaura el bit después de que la dirección de retorno se ha cargado al contador de programa.

La segunda forma es activar el bit con la instrucción:

SEI ;Set Interrupt EnabledRET ;Volver

Esto no es igual que con RETI ya que las interrupciones posteriores ya están habilitadas antes que el contador de programa se cargue con la dirección de retorno. Si otra interrupción está pendiente, su ejecución ya ha comenzado antes que la dirección de retorno se haya extraído de la pila. Dos o más direcciones estarían anidadas en la pila. Normalmente no tiene porqué producir error, pero es un ries-go innecesario. Es mejor utilizar RETI y evitar este flujo innecesario en la pila.

Un vector de interrupción sólo puede contener una sóla instrucción de salto con respecto a la rutina de servicio. Si una interrupción está indefinida o no se utiliza hay que asociarla con una instrucción RETI, para el caso de que suceda la interrupción erróneamente antes de haber escrito la rutina de servicio. En algunos casos es absolutamente necesario para responder a estas falsas interrupciones. Puede darse el caso de que la respectiva rutina de servicio no restablezca automáticamente la ban-dera de interrupción del periférico. Entonces una simple RETI restablecería la interrupción que de otro modo no acabaría nunca. Esto se ds con algunas de las interrupciones del UART.

Tenga en cuenta que dispositivos grandes tienen una organización de palabra doble en la tabla de vectores. En estos casos, en lugar de RJMP, debe ser utilizada la instrucción JMP y la instrucción RETI seguida por una NOP para que apunte a la dirección siguiente de la tabla de vectores.

Como después de que entra una interrupción, la ejecución de las interrupciones de menor prioridad está bloqueada, las rutinas de servicio a interrupción deben ser lo más cortas posible. Si usted nece-sita tener una rutina grande para servir a la interrupción, utilice uno de los dos métodos siguientes. El primero es permitir interrupciones con SEI (Set Interrupt Enabled) en la rutina de servicio cada vez que haya terminado con las tareas más urgentes. Aunque no es muy inteligente. Es más conveniente llevar a cabo las tareas urgentes, establecer la bandera en un registro en algún lugar de reacción

36

Page 37: Ensamblador AVR

lenta y regresar a la interrupción inmediatamente.

Una regla importante para las rutinas de servicio de int. es:

La primera instrucción es siempre para salvar los indicadores de estado del procesador en un registro o en la pila.

Haga esto antes de usar las instrucciones que podrían cambiar las banderas del registro de estado.La razón es que el programa principal interrumpido sólo podría estar en un estado mediante los indicadores de estado y la interrupción acaba de cambiar las banderas a otro estado. Cosas diverti-das que ocurren de vez en cuando. Por lo tanto, la última instrucción antes de RETI es copiar las banderas en un registro para traerlas de vuelta al puerto de estado o poner el contenido del registro de estado en la pila para restaurarlo a su estado anterior. A continuación se muestran ejemplos de cómo hacerlo:

Salvando en un registro:

Isr:IN R15,SREG ;guardar banderas[... more instructions...]

OUT SREG,R15 ;restaurar banderas

RETI ;volver de la interrupción

Salvando en la pila:

Isr:PUSH R15 ;salvando el registro en la pilaIN R15, SREG

[...more instructions...]OUT SREG,R15 ;restaurar banderasPOP R15RETI ;volver de la interrupción

El método de abajo es más lento. El método de arriba requiere un registro exclusivamente a tal fin.

En general: Todos los registros utilizados en una rutina de servicio deben ser reservados exclusiva-mente a este efecto o deben ser guardados en la pila y restaurados a finales de la rutina de servicio. Dentro de una rutina de servicio de int., nunca cambie el contenido de un registro que es utilizado en alguna otra parte del programa, sin restaurarlo.

Debido a estos requisitos básicos, un ejemplo más sofisticado para una rutina de servicio de int.:

.CSEG ;aqui empieza un segmento de código

.ORG 0000 ;en la dirección ceroRJMP Start ;el vector de reset en la dirección 0000RJMP IService ;0001: primer vector de int., rutina de servicio INT0

[…] otros vectoresStart: ;aquí comienza el programa principal[...] aquí para la definición de la pila y otras cosas

IService: ;aquí comienza la rutina de servicio de int.PUSH R16 ;guardar registro enla pilaIN R16,SREG ;leer el registro de estadoPUSH R16 ;y ponerlo en la pila

[...] aquí la rutina de servicio de int. hace algo y utiliza el registro R16POP R16 ;obtener el registro de bandera anterior de la pila

37

Page 38: Ensamblador AVR

OUT SREG,R16 ;restaurar el anterior estadoPOP R16 ;obtener el contenido anterior de R16 de la pilaRETI ;y retornar desde int.

Parece un poco complicado, pero es un requisito previo para usar interrupciones sin que se produz-can errores graves. Saltar PUSH R16 y POP R16 si no se puede permitir reservar el registro para uso exclusivo dentro de la rutina de servicio. Como una rutina de servicio de int. no puede ser inte-rrumpida (a no ser que se permitan otras interrupciones dentro de la rutina) todas las diferentes ruti-nas de servicio de int. pueden utilizar el mismo registro.

Ahora deber ser claro por qué permitir interrupciones dentro de una rutina de servicio de int. y no al final con RETI no es una buena idea.

Cálculos

Aquí hablaremos de todas las instrucciones necesarias para el cálculo en lenguaje ensamblador AVR. Esto incluye los sistemas de numeración, ajustes de bits, desplazamiento y rotación, sumar, restar o comparar y formatos de conversión.

Sistemas de numeración en ensamblador

Los siguientes formatos de números son comunes en ensamblador:

– Números enteros positivos (bytes, palabras, palabras largas, etc)– Números enteros con signo (enteros pequeños y grandes)– Dígitos codificados en binario (BCD)– BCD empacado.– ASCII con formato numérico.

Si usted viene de un lenguaje de alto nivel olvide los formatos predefinidos de número. El ensambla-dor no tiene ese concepto ni sus limitaciones: usted es el dueño de su propio formato.

Números enteros positivos (bytes, palabras, etc)

El número entero menor que se maneja en ensamblador es un byte de ocho bits. Éste código numérico abarca desde el 0 al 255. Estos bytes encajan exactamente en un registro del MCU. Los números más grandes deben estar basados en este formato base, com más de un registro. Dos bytes producen una palabra (desde 0 a 65.535), tres bytes forman una palabra larga (desde 0 a 16.777.215) y cuatro bytes forman una palabra doble (desde 0 a 4.294.967.295).

Un byte, una palabra o una palabra doble se pueden almacenar en cualquier registro que se elija. Las operaciones con estos bytes se programan byte a byte, por lo que no hay que ponerlos en una fila. Formar una fila para almacenar una palabra doble podría ser así:

.DEF r16 = dw0

.DEF r17 = dw1

.DEF r18 = dw2

.DEF r19 = dw3

Los registros dw0 a dw3 están en una fila pero no es necesario. Si tenemos que iniciar esta palabra doble (por ejemplo, 4.000.000) al comienzo de una aplicación, puede tener este aspecto:

.EQU dwi = 4000000 ;se define la constanteLDI dw0,LOW(dwi) ;LSB a R16LDI dw1,BYTE2(dwi) ;bits 8 .. 15 a R17LDI dw2,BYTE3(dwi) ;bits 16 .. 23 a R18LDI dw3,BYTE4(dwi) ;bits 24 .. 31 a R19

38

Page 39: Ensamblador AVR

Hemos dividido este número decimal, llamado dwi, en sus partes binarias byte4 a byte1 y empacado en un paquete de 4 bytes. Ahora se pueden hacer cálculos con esta palabra doble.

Números enteros con signo

A veces, aunque en casos raros, se necesita hacer cálculos con números negativos. Un número negativo se define por interpretar el bit más significativo de un byte como bit de signo. Si es 0 el número es positivo. Si es 1 es negativo. Si el número es negativo por lo general no se almacena el resto del número tal como está, sino que usamos su valor invertido. Invertido significa que -1 no se escribe como 1000.0001, sino como 1111.1111. Esto significa: restar 1 a 0 (y olvidar el desborda-miento). El primer bit es el bit de signo, lo que indica que se trata de un número negativo. Se utiliza este formato diferente (restar el número de 0) porque la suma de -1 (1111.1111) y 1 (0000.0001) da exactamente cero, si se olvida el desbordamiento producido con la operación (el noveno bit).

En un byte el número entero más grande que se maneja es 127 (binario 01111111)y el más pequeño es -128 (binario 10000000). En otros lenguajes de programación este formato de número se llama entero corto. Si usted necesita un mayor rango de valores puede añadir otro byte para formar un valor entero más grande, que va desde 32.767 .. -32.768, cuatro bytes proporcionan un rango desde 2147483647 .. -2147483648, llamado en otros lenguajes entero largo o doble.

Dígitos en codificación binaria, BCD

Hemos hablado anteriormente del formato más eficaz para usar el espacio disponible con números enteros positivos o con signo. Otro formato menos denso y más fácil de entender y manejar es almacenar números decimales en bytes, un dígito en cada byte. El dígito decimal se almacena en su formato binario en un byte. Cada dígito comprendido entre 0 .. 9 necesita 4 bits (valores binarios 0000 .. 1001) y los cuatro bits superiores del byte son siempre ceros. Para manejar el valor 250 se necesitan por lo menos tres bytes, por ejemplo:

Valor de bit 128 64 32 16 8 4 2 1

R16, Digito 1 = 2 0 0 0 0 0 0 1 0

R17, Digito 2 = 5 0 0 0 0 0 1 0 1

R18, Digito 3 = 0 0 0 0 0 0 0 0 0

;Iinstrucciones de uso:LDI R16,2LDI R17,5LDI R18,0

Se puede hacer cálculos con estos números, pero es un poco más complicado en ensamblador que calcular con valores binarios. Las ventajas de este formato es que se pueden manejar números tan grandes como se quiera, siempre y cuando haya suficiente espacio de almacenamiento. Los cálculos son tan precisos como haga falta (por ejemplo, si programa el AVR para aplicaciones de banca), y se pueden convertir muy fácilmente a cadenas de caracteres.

BCD empacado

Si se empaquetan dos dígitos decimales en un byte no se pierde tanto espacio de almacenamiento. Este método se llama empacado de dígitos en codificación binaria. Las dos parte de un byte se llaman nibble superior e inferior. El nible superior por lo general tiene el dígito más significativo, que conlleva ventajas para hacer los cálculos (instrucciones especiales en ensamblador AVR). El número decimal 250 luciría así en el formato BCD empacado:

39

Page 40: Ensamblador AVR

Byte dígito valor 8 4 2 1 8 4 2 1

2 4 y 3 02 0 0 0 0 0 0 1 0

1 2 y 1 50 0 1 0 1 0 0 0 0

;Instrucciones para configurar:LDI R17,0x02 ;byte superiorLDI R16,0x50 ;byte inferior

Para establecer los bits apropiados a su posición de nibble correcta puede utilizar la notación binaria (0b...) o la hexadecimal (0x...).

Los cálculos con BCD embalado son un poco más complejos comparándolos con la forma binaria. El cambios de formato a cadenas de caracteres es casi tan fácil como con BCD y la longitud de los números y la precisión de los cálculos sólo están limitadas por el espacio de almacenamiento.

Números en formato ASCII (Código Estándar Americano para Intercambio de Información)

Muy similar al formato BCD desempaquetado es almacenar números en formato ASCII. Los dígitos del 0 al 9 se almacenan utilizando su representación en ASCII. Este es un formato muy antiguo, desarrollado y optimizado para los teletipos, innecesariamente complicado para el uso con ordenador y muy limitado para un rango de distintos idiomas (sólo 7 bits por carácter). Hoy en día todavía se utiliza mucho en las comunicaciones debido a que no se ha echo un esfuerzo por parte de los progra-madores de sistemas operativos para encontrar un sistema de caracteres más eficaz. Este antiguo sistema sólo es superado por el conjunto de caracteres de teletipo europeo de 5 bits llamado Baudot o por el código Morse, que todavía es utilizado por algunas personas de dedos nerviosos.

En el sistema de código ASCII el dígito 0 decimal es representado por el número 48 (0x30 hexadeci-mal, 0b0011.0000 binario) y el dígito 9 por el 57 (0x39 hexadecimal, 0b0011.1001 binario). ASCII no fue diseñado al principio para tener estos números en el conjunto de código ya que existen caracte-res instrucción como EOT (End Of Transmission) para el teletipo. Así que hay que sumar 48 a un BCD (o establecer los bits 4 y 5 a 1) para convertir un BCD a ASCII. Cargar el número 250 a un conjunto de registros que representarían a este número se vería asi:

LDI R18,'2'LDI R17,'5'LDI R16,'0'

La representación ASCII de estos caracteres se escribirían en los registros.

Manipulación de bits

Para convertir un número BCD en su representación en ASCII necesitamos establecer los bits 4 y 5 a 1. En otras palabras, necesitamos hacer una operación OR al BCD con un valor constante 0x30 hexadecimal. En ensamblador esto se hace así:

ORI R16,0x30

Si tenemos un registro que ya está establecido en 0x30 hexadecimal podemos hacer el OR con este registro para convertir el BCD:

OR R1,R2

Traer un carácter ESCII de un BCD es también fácil. La instrucción

ANDI R16,0x0F

40

Page 41: Ensamblador AVR

aisla los 4 bits bajos (=el nibble inferior). Tenga en cuenta que ORI y ANDI sólo son posibles con registros por encima de R15, utilice los registros desde R16 a R31.

Si el valor hexadecimal 0x0F ya está en el registro R2, puede hacer AND al carácter ASCII con este registro:

AND R1,R2

Las otras instrucciones para manipulación de bits en un registro también están limitadas para los registros por encima de R15. Se formularían de esta manera:

SBR R16,0b00110000 ;poner bits 4 y 5 a unoCBR R16,0b00110000 ;poner bits 4 y 5 a cero

Si uno o más bits de un byte tienen que ser invertidos puede utilizar la siguiente instrucción (que no es válida para usarla con una constante):

LDI R16,0b10101010 ;invertir todos los bits imparesEOR R1,R16 ;en el registro R1 guardando el resultado en el mismo

Para invertir todos los bits de un byte (se llama complemento a uno):

COM R1

que invierte el contenido en el registro R1 y reemplaza los ceros por uno y viceversa. A diferencia de que del complemento a dos, que convierte un número positivo con signo a su complemento negativo (restando de cero). Esto se hace con la instrucción:

NEG R1

Así que +1 (decimal: 1) produce -1 (1.1111111 en binario), +2 produce -2 (1.1111110 en binario), y así sucesivamente.

Además de la manipulación de los bits en un registro, es posible la copia de un sólo bit con el denominado T-bit del registro de estado. Con

BST R1,0

el bit T se carga con una copia del bit 0 del registro R1. El bit T puede ser activado o desactivado y su contenido se puede copiar a cualquier bit de cualquier registro:

CLT ;poner a cero el T-bit, oSET ;poner a 1 el T-bit, oBLD R2,2 ;copiar el T-bit al bit 2 del registro R2

Desplazamiento y rotación

El desplazamiento y la rotación de números binarios se hace mediante el método de multiplicar y dividir por dos. El desplazamiento tiene varias sub-instrucciones.

La multiplicación por dos se realiza desplazando todos los bits de un byte una posición a la izquierda y escribiendo un cero en el bit menos significativo. Esto se conoce como desplazamiento lógico a la izquierda o LSL (logical shift left). El antiguo bit 7 del byte se lleva al bit de acarreo del registro de estado.

LSL R1

41

Page 42: Ensamblador AVR

La división inversa por dos es la instrucción llamada LSR (logical shift right) desplazamiento lógico a la derecha.

LSR R1

Aquí, en el antiguo bit 7, que ahora es el bit 6, se pone un cero, y el antiguo bit de la posición 0 se lleva al bit de acarreo del registro de estado. El bit de acarreo puede ser utilizado para redondear hacia arriba y hacia abajo (Si está en 1 se suma 1 al resultado). Por ejemplo, la división por cuatro con redondeo:

LSR R1 ;división por 2BRCC Div2 ;saltar si no se redondeaINC R1 ;redondear

Div2:LSR R1 ;otra vez división por 2BRCC DivE ;saltar si no se redondeaINC R1 ;redondear

DivE:

Así, la división es fácil con los binarios siempre y cuando se divida por múltiplos de 2.

Si se usan enteros con signo, el desplazamiento lógico a la derecha sobreescribirá el bit 7 de signo. La instrucción ASR (arithmetic shift right) desplazamiento aritmético a la derecha, deja al bit 7 sin tocar, desplazando los 7 bits bajos e inserta un cero en el bit 6.

ASR R1

Al igual que con el desplazamiento lógico, el antiguo bit 0 se lleva al bit de acarreo del registro de estado.

¿Qué hay acerca de multiplicar por 2 una palabra de 16 bits? El bit más significativo del byte menor se desplaza para obtener el bit más bajo del byte mayor. En este paso se establece el bit más bajo a cero, pero tenemos que cambiar el bit de acarreo del paso anterior del byte inferior por el bit 0 del byte superior. Esto se denomina rotación. Durante la rotación, el bit de acarreo del registro de estado se desplaza al bit 0 y el antiguo bit 7 se desplaza al bit de acarreo.

LSL R1 ;Desplazamiento lógico a la izquierda del byte inferiorROL R2 ;Rotación a la izquierda del byte superior (Rotate Left)

La primera instrucción es el desplazamiento lógico a la izquierda que desplaza el bit 7 al bit de acarreo. La instrucción ROL rota el bit 0 del byte superior. Después de la 2ª instrucción el bit de acarreo tiene el antiguo bit 7 del byte superior, que puede ser utilizado tanto para indicar un exceso de capacidad o desbordamiento (en el caso de cálculos de 16 bits) o para rotar en siguientes bytes superiores (si los cálculos son de más de 16 bits).

También es posible la rotación a la derecha, dividiendo por 2 y desplazando al acarreo el bit 7 del resultado:

LSR R2 ;desplazamiento lógico a la derecha, el bit 0 al acarreoROR R1 ;Rotación a la derecha y desplazar el bit al acarreo

Como se ve es fácil dividir con números grandes y el aprendizaje de ensamblador no es tan complicado.

La última instrucción, ROR (Rotate Right), se utiliza con mucha frecuencia para desplazar cuatro bits en un solo paso con los paquetes BCDs. Esta instrucción cambia un nibble superior por el inferior y viceversa. En el ejemplo tenemos que cambiar el nibble superior a la posición del nibble inferior. En

42

Page 43: Ensamblador AVR

lugar de hacer

ROR R1ROR R1ROR R1ROR R1

se puede hacer con una sola instrucción

SWAP R1

Esta instrucción intercambia el nibble superior por el nibble inferior. Tener en cuenta que el contenido del nibble superior será diferente según se aplique uno u otro de estos dos métodos.

Sumar, restar y comparar

Sumar y restar números de 16 bits

Para empezar complicadamente, se van a sumar dos números de 16 bits en los registros R1:R2 y R3:R4 (en esta notación, el primer registro es el byte más significativo y el segundo el menos significativo).

ADD R2,R4 ;primero se suman los dos bytes bajosADC R1,R3 ;a continuación los dos bytes bajos+

En lugar de un segundo ADD usamos ADC en la segunda instrucción, que significa sumar y llevar o acarrear, que se establece o no durante la primera instrucción, en función del resultado.

Para restar R3:R4 de R1:R2

SUB R2,R4 ;primero el byte bajoSBC R1,R3 ;después el byte alto

Otra vez el mismo truco: en la segunda instrucción restamos otro 1 si en el resultado de la primera ha habido un exceso de capacidad o desbordamiento.

Comparar números de 16 bits

Ahora comparamos la palabra de 16 bits en R1:R2 con la que hay en R3:R4 para evaluar si es más grande. En lugar de SUB utilizamos la instrucción de comparación CP y en lugar de SBC utilizamos CPC:

CP R2,R4 ;compara los bytes bajosCPC R1,R3 ;compara los bytes altos

Si ahora se establece o pone a 1 la bandera de acarreo R1:R2 es más pequeño que R3:R4.

Comparar con constantes

Ahora se compara el contenido del registro R16 con una constante: 0b10101010.

CPI R16,0xAA

Si después de esta operación el bit Zero del registro de estado se establece en 1, el registro R16 es igual a 0xAA. Si se establece el bit de acarreo, como ya sabemos, es que es más pequeño. Si no se ha establecido ninguno de estos dos bits en el registro de estado, es que es más grande.

43

Page 44: Ensamblador AVR

Para comprobar si por ejemplo R1 es cero o negativo

TST R1

Si se establece en 1 el Z-bit, el registro R1 es cero y podemos seguir con las instrucciones BREQ, BRNE, BRMI, BRPL, BRLO, BRSH, BRGE, BRLT, BRVC o BRVS de ramificación alrededor de un pequeño bit.

Matemáticas con paquetes BCD

Ahora algunos cálculos con paquetes BCD. La suma de dos paquetes BCD puede dar lugar a dos desbordamientos distintos. El acarreo habitual muestra un exceso de capacidad, si el nibble mayor es más de 15 decimal. El otro desbordamiento se produce del nibble inferior al superior, si al sumar dos nibbles bajos da más de 15 decimal.

Por ejemplo, podemos sumar 2 BCDs empacados como 49 (=hex 49) y 99 (=hex 99) para producir 148 (=hex 148). Esta suma en cálculo binario da como resultado un byte con un valor hexadecimal de 0xE2, sin producirse desbordamiento del byte. Pero en el nibble inferior si se produce desbordamiento porque 9+9=18 y el nibble inferior solo puede manejar números hasta 15. El desbordamiento se añade al bit 4, el bit menos significativo del nibble superior, que es correcto, pero el nibble inferior debe estar en 8 y está a sólo 2 (18 = 0b0001.0010). Hay que sumar 6 a dicho nibble para obtener el resultado correcto. Esto es lógico porque cada vez que el nibble inferior alcanza más de 9 hay que sumarle 6 para corregirle.

El nibble superior es totalmente incorrecto, porque es 0xE y debe ser de 3 (con un desbordamiento en el siguiente dígito superior del BCD empacado). Si sumamos 6 a 0xE obtenemos 0x4 y se esta-blece el acarreo (=0x14). Así que el truco está en primero sumar estos dos números y a continuación añadir 0x66 para corregir los 2 dígitos del BCD empacado. Pero un momento, ¿y si la suma del primer y segundo número no da lugar a un desbordamiento del siguiente nibble, es decir no da una cifra superior a 9 en el nibble inferior? Añadir 0x66 produciría entonces un resultado totalmente incorrecto. Solamente se le añade 6 al nibble inferior si este se desborda al nibble superior por una cifra superior a 9. Lo mismo con el nibble superior.

¿Cómo sabemos si se produce un desbordamiento del nibble inferior al superior? El MCU establece a 1 el bit H en el registro de estado, que es el bit de acarreo a la mitad. A continuación se muestra el algo-ritmo para los diferentes casos posibles después de sumar dos nibbles y después de sumar 0x6 hex.

1. Sumar los nibbles. Si ocurre un desbordamiento (C para el nibble superior o H para el nibble inferior) sumar 6 para corregir, sino, hacer el paso 2.

2. Sumar 6 al nibble. Si ocurre un desbordamiento (C resp. H) ya está hecho. Si no, restar 6.

Para programar un ejemplo, supongamos que tenemos 2 BCDs empacados en R2 y R3, R1 realizará el desbordamiento y R16 y R17 están disponibles para los cálculos. R16 es el registro de la adición pa-ra sumar 0x66 (con el registro R2 no se puede sumar un valor constante) y R17 se utiliza para corregir el resultado en función de las correspondientes banderas. La suma de R2 y R3 se podría ver así:

LDI R16,0x66 ;para sumar 0x66 al resultadoLDI R17,0x66 ;para luego restar del resultadoADD R2,R3 ;sumar los dos digitos BCDsBRCC NoCy1 ;saltar a NoCy1 si no se produce desbordamiento de byteINC R1 ;incrementar el siguiente byte superiorANDI R17,0x0F ;no restar 6 al nibble alto

NoCy1:BRHC NoHc1 ;ir si no ha ocurrido un acarreo a la mitadANDI R17,0xF0 ;no restar 6 del nibble bajo

NoHc1:

44

Page 45: Ensamblador AVR

ADD R2,R16 ;sumar 0x66 al resultadoBRCC NoCy2 ;ir si no a habido acarreoINC R1 ;incrementar el siguiente byte superiorANDI R17,0x0F ;no restar 6 del nibble alto

NoCy2:BRHC NoHc2 ;ir si no ha ocurrido un acarreo a la mitadANDI R17,0xF0 ;no restar 6 del nibble bajo

NoHc2:SUB R2,R17 ;restar la corrección

Un poco más corto:

ADD R2,R16ADD R2,R3BRCC NoCyINC R1ANDI R16,0x0F

NoCy:BRHC NoHcANDI R16,0xF0

NoHc:SUB R2,R16

Pregunta para pensar: ¿Por qué es igualmente correcto, la mitad de corto y menos complicado, y dónde está el truco?

Conversión de formato de números

Cualquier formato numérico se puede convertir a cualquier otro formato. Ya se vió anteriormente la conversión de BCD a ASCII (manipulación de bits). Ahora veremos otras.

Conversión de BCD empacado a BCD, ASCII o Binario

La conversión de paquetes BCD no es complicada. Primero tenemos que copiar el número a otro registro. Al valor copiado le intercambiamos los nibbles con la instrucción SWAP. La parte superior se despeja a cero por ejemplo haciéndole una operación AND con 0x0F y nos queda el nibble el antiguo nibble superior que se puede utilizar tal cual (BCD) o estableciendo el bit 4 y 5 a 1 para convertirlo al juego de caracteres ASCII. Después volvemos a copiar el byte y tratamos con el nibble inferior sin hacerle intercambio como antes y obtenemos el BCD inferior.

La conversión de dígitos BCD a binario es más complicada. Dependiendo de los números que se ma-nejen, primero hay que despejar a cero los bytes necesarios para hacer la conversión. A continuación comenzamos con los dígitos BCD más altos que hay que multiplicarlos por 10. Con el fin de hacer esta multiplicación, hay que copiar el resultado en otro lugar. A continuación se multiplica el resultado por 4 (dos desplazamientos a la izquierda). Sumando el número copiado anteriormente a esto produce una multiplicación por 5. Ahora, una multiplicación por 2 (un desplazamiento a la izquierda) produce el re-sultado multiplicado por 10. Por último se añade el BCD y se repite el algoritmo hasta que se han con-vertido todos los dígitos decimales. Si durante una de estas operaciones se produce un acarreo del re-sultado, el BCD es demasiado grande para ser convertido, por lo que este algoritmo maneja números de cualquier longitud, siempre y cuando estén preparados los registros para los resultados.

[Conversión de un número decimal de 5 digitos en BCD a binario natural de 16 bits.;El máximo número a convertir será pues el b'1111111111111111'=0xFFFF=d'65535'

; El metodo usado para la conversión de un número BCD a binario natural, se basa en que cada dígito de un número codificado en BCD tiene un peso igual a la potencia de diez asociada a su posición. Para convertir el número BCD a su equivalente binario sólo es necesario multiplicar cada dígito BCD por su peso correspondiente y luego sumar todos los productos parciales obtenidos, el resultado es el número binario natural buscado.

45

Page 46: Ensamblador AVR

; Es decir el valor de un número BCD de 5 dígitos se puede expresar como:; 10^4 DecenasMillar + 10^3 Millares + 10^2 Centenas + 10 Decenas + Unidades =10*10*10*10 DecenasMillar + 10*10*10 Millares + 10*10 Centenas + 10 Decenas + Unidades;; Finalmente una subrutina aplica el siguiente algoritmo:

10(10(10(10 DecenasMillar + Millares) + Centenas) + Decenas) + Unidades = Resultado;; El resultado se obtiene en 16 bits, es decir en 2 registros de 8 bits.]

Conversión de binario a BCD

La conversión de binario a BCD es menos complicado. Para convertir 16 bits en binario restamos 10.000 (0x2710) hasta que se produce un desbordamiento, dando el primer dígito. A continuación repetimos con 1.000 (0x03E8) dando el segundo dígito y así sucesivamente con 100 (0x0064), 10 (0x000A) y el resto es el último dígito. Las constantes 10.000, 1.000, 100 y 10 se pueden almacenar en palabras en la memoria de programa y organizadas en una tabla:

DezTab:.DW 10000, 1000, 100, 10

y así pueden ser leídas de la tabla por palabra con la instrucción LPM.

Una alternativa es una tabla que contenga el valor decimal de cada bit en 16 bits binarios:

.DB 0,3,2,7,6,8

.DB 0,1,6,3,8,4

.DB 0,0,8,1,9,2

.DB 0,0,4,0,9,6

.DB 0,0,2,0,4,8 ;y así sucesivamente hasta

.DB 0,0,0,0,0,1

A continuación se cambian los bits individuales en binario desplazando hacia el registro de la izquierda el acarreo. Si es un uno, se suma al número de la tabla resultado de la instrucción LPM. Esto es más complicado de programar y un poco más lento.

Un tercer método consiste en calcular el valor de la tabla partiendo de 000001, mediante la suma del BCD consigo mismo, cada vez después de haber cambiado a la derecha un bit al binario y sumándole el resultado BCD.

Hay varios métodos y hay que aplicar el más optimo.

Multiplicación

Para multiplicar 2 binarios de 8 bits, se puede hacer como con números decimales:

1234 * 567 = ?--------------------------------------------

1234 * 7 = 8638+ 1234 * 60 = 74040+ 1234 * 500 = 617000--------------------------------------------

1234 * 567 = 699678=========================

46

Page 47: Ensamblador AVR

Estos son los pasos del ejemplo:

– Se multiplica el primer número por la cifra menos significativa del segundo número.– Se vuelve a multiplicar el primer número por 10 y por el siguiente dígito significativo del

segundo número y se suma el resultado al anterior.– Y se vuelve a multiplicar el primer número por 100 y por el siguiente dígito (ahora el más

significativo) del segundo número y se suma el resultado al anterior obteniéndose el resultado final.

Multiplicación binaria

Ahora en binario. La multiplicación por 10 en decimal lleva a la multiplicación por 2 en binario. Esta multiplicación se hace facilmente ya sea por la suma de la cantidad a si misma o bien por el despla-zamiento de todos los bits una posición a la izquierda y añadiendo un cero a la derecha.

Programa en ensamblador AVREn el código fuente siguiente se muestra la multiplicación en ensamblador.

; Mult8.asm multiplica 2 números de 8 bits produciendo un resultado de 16 bits;.NOLIST.INCLUDE "C:\avrtools\appnotes\8515def.inc".LIST;; Flujo de la multiplicación;; 1.Los binarios multiplicados se desplazan bit a bit en el bit de acarreo. Si es un uno se suma al resultado y ; si no es uno cambia de posición y no se suma; 2.El número binario se multiplica por 2 y se desplaza una posición a la izquierda poniendo un cero en la ; posición derecha que queda vacía.; 3.Si el binario a multiplicar no es cero, se repite el ciclo.;; Registros usados;.DEF rm1 = R0 ; el número binario a multiplicar (8 Bit).DEF rmh = R1 ; almacenamiento temporal.DEF rm2 = R2 ; el número binario con el que se va a multiplicar (8 Bit).DEF rel = R3 ; Resultado LSB (16 Bit).DEF reh = R4 ; Resultado, MSB.DEF rmp = R16 ; Registro multipropósito para la carga;.CSEG.ORG 0000;

rjmp START;START:

ldi rmp,0xAA ; ejemplo binario 1010.1010 mov rm1,rmp ; ten el primer registro ldi rmp,0x55 ; ejemplo binario 0101.0101 mov rm2,rmp ; en el segundo registro

;; Aquí empieza la multiplicación de los dos binarios en rm1 y rm2, el resultado irá a reh:rel (16 bits);MULT8:;; Valores de inicio despejados a cero

clr rmh ; almacenamiento temporal despejadoclr rel ; registros de resultados despejadosclr reh

47

Page 48: Ensamblador AVR

;; Aquí comienza el ciclo de multiplicación;MULT8a:;; Paso 1: se rota el bit más bajo del segundo número binario a la bandera de acarreo (se divide por 2 y se rota ; un cero al bit 7);

clc ; a cero el bit de acarreoror rm2 ; bit 0 al acarreo, bits 1 a 7 una posición a la derecha, el valor en el acarreo al bit 7

;; Paso 2: Bifurcación dependiendo si es un cero o un uno lo que se ha rotado al bit de acarreo;

brcc MULT8b ; saltar la suma si el acarreo es cero;; Paso 3: Sumar los 16 bits en rmh:rml al resultado con el desbordamiento desde el LSB al MSB;

add rel,rm1 ; sumar el LSB de rm1 al resultadoadc reh,rmh ; sumar el acarreo y el MSB a rm1

;MULT8b:;; Paso 4: Multiplicar rmh:rm1 por 2 (16 bits, desplazamiento a la izquierda);

clc ; a cero el bit de acarreo rol rm1 ; rotar LSB a la izquierda (multiplicar por 2) rol rmh ; desplazar el MSB una posición a la izquierda y llevar el acarreo

;; Paso 5: Comprobar si todavía hay unos en el segundo número binario y si los hay, a multiplicar;

tst rm2 ; ¿Todos los bits a cero? brne MULT8a ; si no, seguir en el bucle

;; Finalización dela multiplicación, resultado en reh:rel;; bucle sin fin;LOOP:

rjmp loop

Rotación binaria

Para entender la operación de multiplica-ción, es necesario comprender las instruc-ciones de rotación binaria ROL y ROR. Estas instrucciones desplazan todos los bits de un registro una posición a la izquierda (ROL) o a la derecha (ROR). La posición que queda vacía se llena con el contenido del bit de acarreo del registro de estado y el bit que sale del registro se desplaza al bit de acarreo. Esta operación se demuestra utilizando 0xAA como ejemplo para ROL, y 0x55 como ejemplo para ROR.

Hardware de multiplicación

Todos los ATmega, ATXmega, AT90CAN y AT90PWM incluyen un multiplicador de hardware que puede llevar a cabo multiplicaciones de 8 bits en sólo dos ciclos de reloj. Así que cuando haya que hacer multiplicaciones y el software no necesite ejecutarse en chips AT90S -o ATtiny-, se puede

48

Page 49: Ensamblador AVR

utilizar esta característica de hardware.

A continuación se muestra cómo multiplicar

– binarios 8 por 8– “ 16 por 8– “ 16 por 16– “ 16 por 24

Multiplicación por hardware de binarios 8 por 8Se hace de forma simple y directa: Si los dos binarios que se van a mul-tiplicar están en los registros R16 y R17 sólo hay que poner

mul R16,R17 El resultado de la multiplicación de es-tos dos binarios de 8 bits pude ser de hasta 16 bits de longitud, por lo que el resultado se escribirá en los registros R1 (byte más significativo) y R0 (byte menos significativo). Esto es todo.

El programa muestra la simulación en el software Studio. Se multiplica el decimal 250 (hex FA) por el decimal 100 (hex 64), en los registros R16 y R17.

Después de la ejecución, los registros R0 (LSB) y R1 (MSB) contienen el

número 61A8 hex. o 25.000 decimal.Y si. Sólo requiere dos ciclos, o 2 microsegundos con un reloj a 1 Mcs/s.

Multiplicación por hard-ware de binarios 16 por 8

¿Hay que multiplicar un binario más grande? El hardware está limitado a

8, por lo que tenemos que emplear algunas buenas ideas. Para resolver el problema con binarios mayores vamos a ver en primer lugar la combinación de 16 y 8. La comprensión de este concepto ayuda a entender el método por el que será capaz de resolver más tarde el problema de la multiplicación de 32 por 64 bits.Primero las matemáticas: un binario de 16 bits m1M:m1L es simplemente dos binarios de 8 bits m1M y m1L, donde el byte más significativo m1M es multiplicado por 256 decimal o por 100 hex. Recordemos por ejemplo que el decimal 1234 es simplemente (12 multiplicado por 100) más 34, o )1 multiplicado por mil) más (2 multiplicado por 100) más (3 multiplicado por 10) y más 4.

49

Page 50: Ensamblador AVR

Así que el binario de 16 bits m1 es igual a 256*m1M más m1L, donde miM es el MSB y m1L es el LSB. Multiplicar m1 por un binario de 8 bits m2 es formulado matemáticamente:

- m1 * m2 = (256*m1M + m1L) * m2, ó- 256*m1M*m2 + m1L*m2.

Así que sólo tenemos que hacer dos multiplicaciones y sumar ambos resultados. Si ves tres asteriscos en la fórmula, se siente: la multiplicación por 256 en el mundo binario no requiere ningún hardware en absoluto, porque es un simple movimiento en el byte de orden superior. Al igual que la multiplicación en el mundo decimal se hace simplemente moviendo el primer número a la izquierda y escribiendo un cero como dígito menos significativo.

Vamos a un ejemplo práctico. Primero necesitamos algunos registros para

– Cargar los números m1 y m2,– proporcionar espacio para el resultado que podría tener 24 bits de longitud.

;; Prueba de multiplicación por hardware 16 por 8 bits;; Definición de los registros:;.def Res1 = R2.def Res2 = R3.def Res3 = R4.def m1L = R16.def m1M = R17.def m2 = R18

Primero cargamos los números:

;; Cargando los registros;.equ m1 = 10000;ldi m1M,HIGH(m1) ;8 bits altos de m1 en m1Hldi m1L,LOW(m1) ;8 bits bajos de m1 en m1Lldi m2,250 ;constante de 8 bits en m2

Los dos números se cargan en R17:R16 (10000 decimal = 2710 hex) y R18 (250 dec = FA hex).

A continuación, primero se multiplica el LSB:

;; Multiplicando;

mul m1L,m2 ;multiplicando el LSBmov Res1,R0 ;copiando el resultado al registro de resultadomov Res2,R1

50

Page 51: Ensamblador AVR

La multiplicación del LSB, 10 hex por FA hex produce 0FA0 hex, que se escribe en los registros R0 (LSB A0 hex) y R1 (MSB 0F hex). El resultado se copia en los bytes inferiores del registro de resultado, R3:R2.

Multiplicación del MSB de m1 por m2:

mul m1M,m2 ;multiplicando el MSB

La multiplicación del MSB de m1, 27 hex, por m2, FA hex, produce 2616 hex en R1:R0

Ahora se realizan dos pasos a la vez, la multiplicación por 256 y sumar el resultado con el anterior. Esto se hace mediante la suma de R1:R0 a Res3:Res2 en lugar de a Res2:Res1. R1 sólo se puede copiar a Res3 y entonces R0 se suma Res2. Si se establece el acarreo después de la suma, el siguiente byte alto Res3 se incrementa en uno.

mov Res3,R1 ;copia el MSB al resultado del byte 3add Res2,R0 ;y sumar el LSB al resultado del byte 2brcc NoInc ;si no hay acarreo, saltarinc Res3

NoInc:

El resultado en R4:R3:R2 es 2625A0 hex, 2500000 en decimal, lo que es obviamente correcto.

Además, estas operaciones consumen 10 ciclos que con un reloj de 1 Mhz son 10 microsegundos. Mucho más rápido que la multiplicación por software.

Multiplicación por hardware de binarios 16 por 16

Ahora que tenemos claros los principios, debería ser fácil hacer 16 por 16. El resultado ahora requiere 4 bytes (Res4:Res3:Res2:Res1, ubicados en R5:R4:R3:R2). La fórmula sería:

m1 * m2=(256*m1M + m1L) *

(256*m2M + m2L)

=65536*m1M*m2M +

256*m1M*m2L +

256*m1L*m2M +

m1L*m2L

Obviamente ahora hay que hacer 4 multiplicaciones. Comenzamos con la primera y la última que son las más sencillas: sus resultados simplemente se copian en las posiciones correctas de los respectivos registros de resul-tado. Los resultados de las multiplica-ciones de en medio se añadirán a los

51

Page 52: Ensamblador AVR

registros de resultado también de en medio llevándonos los posibles acarreos al byte más significa-tivo. Para esto hay un simple truco fácil de entender:

;; Prueba de multiplicación hardware 16 por 16;; Definición de registros;.def Res1 = R2.def Res2 = R3.def Res3 = R4.def Res4 = R5.def m1L = R16.def m1M = R17.def m2L = R18.def m2M = R19.def tmp = R20;; Cargar valores de entrada;.equ m1 = 10000.equ m2 = 25000;

ldi m1M,HIGH(m1)ldi m1L,LOW(m1)ldi m2M,HIGH(m2)ldi m2L,LOW(m2)

;; Multiplicando;

clr R20 ;Despejar para acarreomul m1M,m2M ;Multiplicar MSBsmov Res3,R0 ;copy to MSW Resultmov Res4,R1mul m1L,m2L ;Multiplicar LSBsmov Res1,R0 ;copy to LSW Resultmov Res2,R1mul m1M,m2L ;Multiplicar 1M con 2Ladd Res2,R0 ;Sumar al resultadoadc Res3,R1adc Res4,tmp ;sumar acarreomul m1L,m2M ;Multiplicar 1L con 2Madd Res2,R0 ;Sumar al resultadoadc Res3,R1adc Res4,tmp

;; Multiplicación hecha;

La simulación muestra los siguientes pasos:

La carga de dos constantes 10000 (2710 hex) y 25000 (61A8 hex) en los registros del espacio superior de registros...

Multiplicación de los dos MSBs (27 y 61 hex) y copia del resultado a los registros R1:R0 desde los registros de resultado superior R5:R4...

52

Page 53: Ensamblador AVR

Multiplicación de los dos LSBs (10 y A8 hex) y copia del resultado en R1:R0 desde los registros de resultado inferior R3:R2...

Multiplicación del MSB de m1 con el LSB de m2 y suma del resultado en R1:R0 de los registros de resultado de los bytes del medio. No hay acarreo...

Multiplicación del LSB de m1 con el MSB de m2 y suma del resultado en R1:R0 de los registros de resultado de los bytes del medio. No hay acarreo. El resultado es 0EE6B280 hex, 250000000 decimal, obviamente correcto.

Para esta multiplicación son necesarios 19 ciclos de reloj, que es mucho más rápido que el software de multiplicación.

También: el tiempo requerido será siempre de exactamente 19 ciclos y no depende de los nú-meros de entrada, como pasa con la multiplica-ción por software que produce acarreo, en la que hay que añadir un cero.

Multiplicación por hardware de binarios 16 por 24

La multiplicación de un binario de 16 bits 'a' con un binario de 24 bits 'b' produce resultados con una longitud de hasta 40 bits. El esquema de multiplica-ción requiere seis multiplica-ciones de 8 por 8 bits suman-do los resultados en la posi-ción apropiada de los registros de resultados.

Código fuente en ensamblador:

; Multiplicación hardware 16 por 24.include "m8def.inc";; Definición de registros.def a1 = R2 ; define 16-bit register.def a2 = R3.def b1 = R4 ; define 24-bit register.def b2 = R5.def b3 = R6.def e1 = R7 ; define 40-bit result register.def e2 = R8.def e3 = R9.def e4 = R10.def e5 = R11.def c0 = R12 ; registro de ayuda para sumar.def rl = R16 ; registro de carga;

53

Page 54: Ensamblador AVR

; Carga de constantes.equ a = 10000 ; multiplicador a, hex 2710.equ b = 1000000 ; multiplicador b, hex 0F4240

ldi rl,BYTE1(a) ; carga de amov a1,rlldi rl,BYTE2(a)mov a2,rlldi rl,BYTE1(b) ; carga de bmov b1,rlldi rl,BYTE2(b)mov b2,rlldi rl,BYTE3(b)mov b3,rl

;; Despejar registros

clr e1 ; Despejando registros de resultadosclr e2clr e3clr e4clr e5clr c0 ; Despejando registro de ayuda

;; Multiplicación

mul a2,b3 ; término 1add e4,R0 ; suamndo al resultadoadc e5,R1mul a2,b2 ; término 2add e3,R0adc e4,R1adc e5,c0 ; (sumando posible acarreo)mul a2,b1 ; término 3add e2,R0adc e3,R1adc e4,c0adc e5,c0mul a1,b3 ; término 4add e3,R0adc e4,R1adc e5,c0mul a1,b2 ; término 5add e2,R0adc e3,R1adc e4,c0adc e5,c0mul a1,b1 ; término 6add e1,R0adc e2,R1adc e3,c0adc e4,c0adc e5,c0

;; hecho.

nop; El resultado debe ser 02540BE400 hex

La ejecución completa requiere

● 10 ciclos de reloj para la carga de las constantes,

● 6 ciclos de reloj para despejar (poner a cero) los registros y

54

Page 55: Ensamblador AVR

● 33 ciclos de reloj para la multiplicación.

División

No, desafortunadamente no existe división por hardware, hay que hacerlo por software.

División decimal

Comenzamos con la división decimal para comprender mejor la división binaria. Se parte de dividir por ejemplo, 5678 entre 12:

5678 : 12 = ?------------------------------------ 4 * 1200 = 4800

--------878

- 7 * 120 = 840 ------

38- 3 * 12 = 36

---- 2

Resultado → 5678 : 12 = 473 con Resto 2================================

División Binaria

En binario, la multiplicación del segundo número como en el ejemplo decimal anterior no es necesa-ria debido al hecho de que sólo tenemos 0 y 1 como dígitos. Los números binarios tienen mucha más cantidad de dígitos que su equivalente decimal, por lo que la transferencia de la división decimal a su equivalente binaria es un poco inconveniente. Así que el programa funciona de una manera diferente.

La división de un número binario de 16 bits entre un binario de 8 bits en ensamblador AVR se muestra en el siguiente código:

; Div8 divide un número de 16 bits entre un número de 8 bits (Prueba: 16-bit-number: 0xAAAA, 8-bit-number: ;; 0x55)

.NOLIST

.INCLUDE "C:\avrtools\appnotes\8515def.inc" ; ¡ajuste la ruta correcta en su sistema!

.LIST; Registro.DEF rd1l = R0 ; LSB del número de 16 bits que se divide.DEF rd1h = R1 ; MSB del número de 16 bits que se divide.DEF rd1u = R2 ; registro provisional.DEF rd2 = R3 ; numero de 8 bits por el que se divide.DEF rel = R4 ; LSB resultado.DEF reh = R5 ; MSB resultado.DEF rmp = R16; registro multiproposito para carga;.CSEG.ORG 0

rjmp startstart:; Cargar los números en los registros apropiados

ldi rmp,0xAA ; 0xAAAA a dividirmov rd1h,rmp mov rd1l,rmp

55

Page 56: Ensamblador AVR

ldi rmp,0x55 ; 0x55 por el que se dividemov rd2,rmp

; Divide rd1h:rd1l by rd2div8:

clr rd1u ; despejar registro provisionalclr reh ; (los registros de resultado clr rel ; también se utilizan para contar hasta 16 para los pasos de la divisióninc rel ; se establece en 1 en el arranque)

; Aquí comienza el ciclo de la divisióndiv8a:

clc ; despejar el bit de acarreorol rd1l ; rotar el bit superior siguiente del númerorol rd1h ; al registro provisional (se multiplica por 2)rol rd1u brcs div8b ; se lleva un uno a la izquierda, por lo que se restacp rd1u,rd2 ; resultado de la división ¿1 ó 0?brcs div8c ; saltar la resta si es menor

div8b: sub rd1u,rd2; restar el número por el que se dividesec ; establecer el bit de acarreo, el resultado es un 1rjmp div8d ; saltar al cambio del bit de resultado

div8c: clc ; despejar el bit de acarreo, el bit resultante es un 0

div8d: rol rel ; rotar el bit de acarreo a los registros de resultadorol reh brcc div8a ; mientras sea cero seguir con el ciclo de división

; Fin de la divisiónstop:

rjmp stop ; bucle sin fin

Pasos del programa durante la división

– Definición y selección de los registros para los números binarios de la división– Preajuste del registro provisional y el par de registros de resultado (los registros de resultado

son preseleccionados en 0x0001).– El binario de 16 bits en rd1h:rd1l se rota bit a bit al registro provisional rd1u (multiplicación por

2) si se rota un 1 a rd1u, el programa bifurca al paso 4 de la resta inmediatamente,– el contenido del registro provisional se compara con el número de 8 bits en rd2, si este es más

pequeño se resta del registro provisional y el bit de acarreo se establece a 1. Si rd2 es mayor, no se hace la resta y el bit de acarreo se establece a 0,

– el contenido de la bandera de acarreo se rota al registro de resultado reh:rel a la derecha,– si es un cero se rota fuera del registro de resultado y se repite el bucle de la división. Si es un 1

se ha completado la división.

Si no se entiende la rotación, esta operación está expuesta en la sección de la multiplicación.

Conversión de números

En ensamblador es muy frecuente la conversión de números porque en los procesadores el cálculo se hace en binario mientras que las personas en un terminal prefieren números decimales. Para que haya comunicación desde el interior de una rutina en ensamblador con una persona, es necesaria la conversión de números. Hay que aprender un poco como se realiza la conversión entre diferentes sistemas numéricos.

Por favor consulte el sitio web en http://www.avr-asm-tutorial.net/avr_en/calc/CONVERSION.htmlsi usted necesita el código fuente o una mejor comprensión.

Las fracciones decimales

56

Page 57: Ensamblador AVR

Primero, no utilice punto flotante a menos que no quede más remedio. El punto flotante deja sin recursos al AVR y necesita un tiempo de ejecución extremo. Si se encuentra en este dilema y piensa que ensamblador es demasiado complicado puede preferir basic u otros lenguajes como C o Pascal. Pero si no es así y prefiere utilizar ensamblador, aquí se muestra como realizar la multiplicación con números reales de punto fijo en menos de 60 microsegundos e incluso, en casos especiales en 18 microsegundos, a 4 MHz de frecuencia de reloj, sin necesidad de trucos caros como extensiones de procesador de punto flotante.

Esto se hace volviendo a las raíces de las matemáticas. La mayoría de las tareas con números reales de coma flotante se pueden hacer utilizando números enteros. Con éstos, es fácil y rápido programar en ensamblador. El punto flotante se queda en el cerebro del programador y se añade en algún lugar de la secuencia de dígitos decimales. Nadie se da cuenta y este es el truco.

Conversiones lineales

A modo de ejemplo: un convertidor AD de 8 bits toma muestras de una señal de entrada en el rango desde 0,00 a 2,55 voltios y devuelve el resultado en sistema binario en el rango desde $00 a $FF. El resultado, una tensión, se muestra en una pantalla LCD. Es un ejemplo muy fácil: el binario se convierte a una cadena decimal ASCII entre 000 y 255, y hay que insertar la coma decimal justo detrás del primer dígito y ¡ya está!

Pero el mundo de la electrónica es a veces más complicado. Por ejemplo, el convertidor AD devuelve un hexadecimal de 8 bits para la tensión de entrada de entre 0,00 y 5,00 voltios. Ahora nos quedamos confusos y no sabemos como proceder. Para mostrar el resultado correcto en la pantalla LCD, tendríamos que multiplicar el binario por el resultado de la división 500/255, que es 1,9608. Este es un número tonto ya que es casi 2, pero sólo casi, y no queremos esta inexactitud de un 2% mientras que tenemos un convertidor AD con un margen de error en torno a 0,25%. Para afrontar esto, se multiplica la entrada por el resultado de 500 / 255 * 256, que es 501,96, y luego se divide por 256. ¿Por qué primero se multiplica por 256 y luego se vuelve a dividir por 256? Para una mayor precisión. Si multiplicamos la entrada por 502 en lugar de por 501,96, el error es del orden del 0,008%, suficiente para nuestro convertidor AD, podemos vivir con eso. Y dividir por 256 es una tarea fácil, porque es una potencia muy conocida de 2. Al dividir con números que son potencia de 2, el AVR se encuentra cómodo y funciona muy rápido. Al dividir entre 256, el AVR es incluso más rápido, porque sólo tenemos que quitar el último byte del número binario, ni siquiera hay desplazamiento o rotación.

La multiplicación de un binario de 8 bits con el binario de 9 bits 502 (1F6 hex) puede tener un resultado superior a 16 bits, así que tenemos que reservar 24 bits o 3 registros para el mismo. Durante la multiplicación, la constante 502 se ha de desplazar a la izquierda (multiplicación por 2) para añadir estas cifras con el resultado y se hace un desplazamiento cada vez que cambia el número de entrada. Como esto puede necesitar hasta 8 desplazamientos a la izquierda, necesitamos otros tres bytes para esta constante. Así que podemos optar por la siguiente combinación de registros para la multiplicación:

Número Valor(ejemplo) Registro

Valor de entrada 255 R1

multiplicando 502 R4:R3:R2

resultado 128,010 R7:R6:R5

Después de colocar el valor 502 (00.01.F6) en los registros R4:R3:R2 y despejar los registros de resultado R7:R6:R5, la multiplicación es la siguiente:

1. Test, si el número de entrada es cero, hemos terminado.

57

Page 58: Ensamblador AVR

2. Si no, en el número de entrada se desplaza a la derecha un bit, que se lleva al acarreo y se coloca un cero en el bit 7. Esta instrucción se llama Logical-Shift-Right o LSR.

3. Si el bit de acarreo es 1, le sumamos el multiplicando (en el paso 1 el valor es 502, en el paso2 es 1004 y así sucesivamente) al resultado. Durante la suma no hay que olvidarse del acarreo, (la suma de R2 a R5 con ADD, la suma de R3 a R6 y R4 a R7 con ADC). Si elbir de acarreo es un cero, no se hace la suma del multiplicando con el resultado y se pasa el siguiente paso.

4. Ahora el multiplicando se multiplica por 2, ya que el siguiente bit desplazado con respecto al número de entrada vale tanto como el doble. Se desplaza R2 a la izquierda (mediante la inserción de un cero en el bit 0) con LSL y el bit 7 se desplaza al acarreo. A continuación se rota el acarreo a R3, desplazando su contenido a la izquierda y el bit 7 al acarreo. Lo mismo con R4.

5. Así terminamos con un dígito del número de entrada y se procede con el paso 1 de nuevo.

El resultado de la multiplicación por 502 ahora está en los registros de resultado R7:R6:R5. Si nos limitamos a pasar por alto el registro R5 (división por 256), ya tenemos el resultado deseado. Para mejorar la precisión, podemos usar el bit 7 de R5 para redondear el resultado. Ahora sólo tenemos que convertir el resultado de su forma binaria a decimal ASCII. Si añadimos el punto decimal en el lugar correcto de la cadena ASCII, tenemos una cadena con el voltaje lista para ser mostrada en el display.

El programa completo, desde el número de entrada hasta la cadena ASCII resultante, requiere entre 79 y 288 ciclos de reloj, dependiendo del número de entrada. Una rutina de punto flotante como ésta en un lenguaje más sofisticado que ensamblador, conlleva más tiempo de conversión, más programa flash y más uso de memoria.

Ejemplo 1: Convertidor AD de 8 bits con punto flotante

; Demostración de conversión de punto flotante en ensamblador, (C) 2003 www.avr-ASM-tutorial.net; La tarea: Se lee un resultado de 8 bits de un convertidor analógico-digital en un rango que va desde 00 a FF ;hexadecimal y hay que convertirlo en un número de punto flotante en el rango de 0,00 a 5,00 voltios.;Esquema del programa:

;1. Multiplicación por 502 (01F6 hex). Se multiplica por 500 y por 256 y se divide por 255 en un sólo paso.;2. Redondear el resultado y omitir el último byte (esto último divide por 256). Antes de omitir el último byte ;hay que coger el bit 7 para el redondeo.;3. Convertir la palabra resultante a ASCII y establecer el signo decimal en el lugar correcto. La palabra ;resultante en el rango de 0 a 500 se muestra en caracteres ASCII como 0,00 a 5,00.

;Registros utilizados:;La rutina utiliza los registros R8..R1 sin salvarlos previamente. También se requiere un registro multipropósito ;al que se le llama rmp localizado en la mitad superior de los registros. Hay que tener cuidado de que este ;registro no entre en conflicto con los registros utilizados en el resto del programa.;Al entrar en la rutina se espera el número de 8 bits en el registro R1. La multiplicación usa los registros ;R4:R3:R2 para mantener el multiplicador 502 (se desplaza a la izquierda máximo 8 veces durante la ;multiplicación). El resultado de la multiplicación se calcula en los registros R7:R6:R5. El resultado de la ;llamada a la división por 256 ignora R5. R7:R6 se redondea en función del bit más alto de R5 y el resultado se ;copia a R2:R1.;La conversión a cadena ASCII utiliza la entrada en R2:R1, los registros R4:R3 como divisor y coloca el ;resultado en R5:R6:R7:R8 (R6 es la parte decimal);Otras convenciones:;La conversión está estructurada en subrutinas con la utilización de la pila. Esta debe funcionar bien para el uso ;en tres niveles (6 bytes de SRAM).;Tiempos de conversión:;La rutina completa requiere como máximo 228 ciclos de reloj (conversión $FF) y mínimo 79 (conversión $00). ;A 4 MHz los tiempos son de 56,75 y 17,75 microsegundos respectivamente.;Definiciones:;Registros

.DEF rmp = R16 registro multipropósito

58

Page 59: Ensamblador AVR

;Avr tipo: Probado para el AT90S8515. Sólo se requiere establecer la pila. Las rutinas trabajan bien con otros ;AT90S.

.NOLIST

.INCLUDE “8515def.inc”

.LIST

;Inicio del programables;Escribe un número en R1 y comienza la rutina de conversión. Sólo para propósitos de prueba.

.CSEG

.ORG $0000 rjmp main

main: ldi rmp,HIGH(RAMEND) ; establecer la pilaout SPH,rmp ldi rmp,LOW(RAMEND) out SPL,rmp ldi rmp,$FF ; Convertir $FF mov R1,rmp rcall fpconv8 ; llamada a la rutina de conversión

no_end: ; bucle sin fin, cuando se hacerjmp no_end

; Envoltorio de rutina de conversión, llamadas a los diferentes pasos de conversiónfpconv8:

rcall fpconv8m ; multiplicar por 502rcall fpconv8r ; redondea y divide por 256rcall fpconv8a ; convierte a cadena ASCIIldi rmp,'.' ; establecer cadena decimalmov R6,rmp ret ; todo hecho

; Subrutina para multiplicar por 502fpconv8m:

clr R4 ; establecer el multiplicador para 502 ldi rmp,$01 mov R3,rmp ldi rmp,$F6 mov R2,rmp clr R7 ; borrar el resultadoclr R6 clr R5

fpconv8m1: or R1,R1 ; comprobar si el números es todo cerosbrne fpconv8m2 ; todavía hay unos, ir a convertirret ; listo, volver

fpconv8m2: lsr R1 ; desplazar el número a la derecha (dividir por 2)brcc fpconv8m3 ; si el bit más bajo fue 0, entonces salto a la sumaadd R5,R2 ; sumar el número en R6:R5:R4:R3 con el resultadoadc R6,R3 adc R7,R4

fpconv8m3: lsl R2 ; multiplicar R4:R3:R2 por 2 rol R3 rol R4 rjmp fpconv8m1 ; repetir para el siguiente bit

; Redondeo del valor en R7:R6 con el bit 7 de R5fpconv8r:

clr rmp ; ponera a cero rmplsl R5 ; rrotar el bit 7 al acarreoadc R6,rmp ; sumar LSB con el acarreoadc R7,rmp ; sumar el MSBm con el acarreo

59

Page 60: Ensamblador AVR

mov R2,R7 ; Copiar el valor a R2:R1 (dividir por 256) mov R1,R6 ret

; Convertir la palabra en R2:R1 a una cadena ASCII en R5:R6:R7:R8fpconv8a:

clr R4 ; Establecer el valor del divisor decimal a 100 ldi rmp,100 mov R3,rmp rcall fpconv8d ; obtener dígitos ASCII por sustracción repetidamov R5,rmp ; establecer cadena de caracteres de la centenaldi rmp,10 ; Establecer el valor del divisor decimal a 10mov R3,rmp rcall fpconv8d ; obtener el siguiente dígito ASCIImov R7,rmp ; establecer cadena de caracteres de la decenaldi rmp,'0' ; convertir el resto a un carácter ASCIIadd rmp,R1 mov R8,rmp ; establecer la cadena de caracteresret

; Convertir la palabra binaria en R2:R1 a dígito decimal restando el valor del divisor en R4:R3 (100, 10)fpconv8d:

ldi rmp,'0' ; Comienza con el valor decimal 0fpconv8d1: cp R1,R3 ; Comparar la palabra con el valor del divisor decimalcpc R2,R4 brcc fpconv8d2 ; Acarreo a cero. Restar el valor del divisorret ; resta hecha

fpconv8d2: sub R1,R3 ; restar el valor del divisorsbc R2,R4 inc rmp ; incrementar un dígitorjmp fpconv8d1 ; una vez más

; Fin de la rutina de prueba de conversión

Ejemplo 2: Convertidor AD de 10 bits con punto flotante

; Demostración de conversión de punto flotante; en ensamblador, (C)2003 www.avr-asm-tutorial.net;; La tarea: Se lee el resultado de 10 bits de un convertidor analógico-digital. El número está en;el rango de 0000 a 03FF hexadecimal. Hay que convertirlo a un número de punto flotante en;el rango de 0,000 a 5,000 voltios.;; Esquema del programa:; 1. Comprobar que el número es menor de $0400, para prevenir desbordamientos no permitidos; durante la siguiente multiplicación.; 2. Multiplicación por 320313 (04E338 hex). Se multiplica por 5000, por 65536 y se divide por 1023; en un sólo paso.; 3. Redondear el resultado y cortar los dos últimos bytes (dividir por 65536). Para redondear se ; ; ; utiliza antes el bit 15.; 4. Convertir la palabra resultante a ASCII y establecer la posición correcta del punto decimal. ; La palabra resultante en el rango de 0 a 5,000 se muestra en caracteres ASCII.;; Registros utilizados:; Las rutinas utilizan los registros R10..R1 sin salvar previamente. También se requiere un registro ;multipropósito al que se le llama rmp localizado en la mitad superior de los registros. Hay que tener ;cuidado de que este ;registro no entre en conflicto con los registros utilizados en el resto del ;programa.;; Al entrar en la rutina el número de 10 bits se espera en el par de registros R2:R1. Si el número es

60

Page 61: Ensamblador AVR

;mayor de $03FF la rutina retorna con el bit de acarreo establecido y la cadena resultante en ;R5:R6:R7:R8:R9:R10 se establece como cadena ASCII con terminación nula “E.EEEE”; La multiplicación utiliza los registros R6:R5:R4:R3 para contener el multiplicador 320313 (que se ;puede desplazar máximo 10 veces durante la multiplicación). El resultado de la multiplicación se ;calcula en los registros R10:R9:R8:R7. El resultado de la llamada a la división por 65536 ignora ;R8:R7 y el resultado en R10:R9 se redondea en función del bit más alto de R8. Por último el ;resultado final se copia en R2:R1.; La conversión a cadena ASCII utiliza la entrada en R2:R1 y el par de registros R4:R3 como divisor ;para la conversión y coloca el resultado como cadena ASCII en R5:R6:R7:R8:R9:R10 ;(null-terminated o terminada en cero);Otras convenciones:;La conversión está estructurada en subrutinas con la utilización de la pila. Esta debe funcionar bien ;para el uso en tres niveles (6 bytes de SRAM).;Tiempos de conversión:; La rutina completa requiere máximo 326 ciclos de reloj (conversión de $03FF) y mínimo 111;(conversión de $0000). A 4 MHz los tiempos son de 81,25 y 27,5 microsegundos respectivamente.;; Definiciones:; Registros.DEF rmp = R16 ; registro multi-propósito;;Avr tipo: Probado para el AT90S8515. Sólo se requiere establecer la pila. Las rutinas trabajan bien ;con otros AT90S.

.NOLIST

.INCLUDE "8515def.inc"

.LIST;; Comienza el programa;; Escribe un número en R2:R1 y comienza la rutina de conversión. Sólo para propósitos de prueba.;.CSEG.ORG $0000

rjmp main;main:

ldi rmp,HIGH(RAMEND) ; Set the stackout SPH,rmpldi rmp,LOW(RAMEND)out SPL,rmpldi rmp,$03 ; Convert $03FFmov R2,rmpldi rmp,$FFmov R1,rmprcall fpconv10 ; call the conversion routine

no_end: ; unlimited loop, when donerjmp no_end

;; Conversion routine wrapper, calls the different conversion steps;fpconv10:

rcall fpconv10c ; Check the input value in R2:R1brcs fpconv10e ; if carry set, set "E.EEE"

rcall fpconv10m ; multiplicate by 320,313rcall fpconv10r ; redondear and divide by 65536

61

Page 62: Ensamblador AVR

rcall fpconv10a ; convert to ASCII stringrjmp fpconv10f ; set decimal point and null-termination

fpconv10e:ldi rmp,'E' ; set error condition to result stringmov R5,rmpmov R7,rmpmov R8,rmpmov R9, rmp

fpconv10f:ldi rmp,'.' ; set decimal pointmov R6,rmpclr rmp ; null-terminate ASCII stringmov R10,rmpret ; all done

;; Subrutina de verificación de entrada;fpconv10c:

ldi rmp,$03 ; compare MSB with 03cp rmp,R2 ; if R2>$03, set carry on returnret

;; Subroutine multiplication by 320,313;; Starting conditions:; +---+---+; | R2+ R1| Input number; +---+---+; +---+---+---+---+; | R6| R5| R4| R3| Multiplicant 320.313 = $00 04 E3 38; | 00| 04| E3| 38|; +---+---+---+---+; +---+---+---+---+; |R10| R9| R8| R7| Result; | 00| 00| 00| 00|; +---+---+---+---+;fpconv10m:

clr R6 ; set the multiplicant to 320.313ldi rmp,$04mov R5,rmpldi rmp,$E3mov R4,rmpldi rmp,$38mov R3,rmpclr R10 ; clear the resultclr R9clr R8clr R7

fpconv10m1:mov rmp,R1 ; check if the number is clearor rmp,R2 ; any bit of the word a one?brne fpconv10m2 ; still one's, go on convertret ; ready, return back

fpconv10m2:lsr R2 ; shift MSB to the right (div by 2)

62

Page 63: Ensamblador AVR

ror R1 ; rotate LSB to the right and set bit 7brcc fpconv10m3 ; if the lowest bit was 0, then skip adding

add R7,R3 ; add the number in R6:R5:R4:R3 to the resultadc R8,R4adc R9,R5adc R10,R6

fpconv10m3:lsl R3 ; multiply R6:R5:R4:R3 by 2rol R4rol R5rol R6rjmp fpconv10m1 ; repeat for next bit

;; Round the value in R10:R9 with the value in bit 7 of R8;fpconv10r:

clr rmp ; put zero to rmplsl R8 ; rotate bit 7 to carryadc R9,rmp ; add LSB with carryadc R10,rmp ; add MSB with carrymov R2,R10 ; copy the value to R2:R1 (divide by 65536)mov R1,R9ret

;; Convert the word in R2:R1 to an ASCII string in R5:R6:R7:R8:R9:R10;; +----+----+; + R2| R1| Input value 0..5,000; +----+----+; +----+----+; | R4| R3 | Decimal divider value; +----+----+; +----+----+----+----+----+-----+; | R5 | R6 | R7| R8| R9| R10| Resulting ASCII string (for input value 5,000); |' 5 ' | ' . ' | '0' | '0' | '0' | $00 | null-terminated; +----+----+----+----+----+-----+;fpconv10a:

ldi rmp,HIGH(1000) ; Set the decimal divider value to 1,000mov R4,rmpldi rmp,LOW(1000)mov R3,rmprcall fpconv10d ;obtener dígitos ASCII por sustracción repetidamov R5,rmp ; establecer la cadena de caracteres del millarclr R4 ; Set the decimal divider value to 100ldi rmp,100mov R3,rmprcall fpconv10d ; get the next ASCII digitmov R7,rmp ; establecer la cadena de caracteres de la centenaldi rmp,10 ; Set the decimal divider value to 10

mov R3,rmprcall fpconv10d ; get the next ASCII digitmov R8,rmp ; establecer la cadena de caracteres de la decenaldi rmp,'0' ; convert the rest to an ASCII charadd rmp,R1mov R9,rmp ; establecer la cadena de la unidad

63

Page 64: Ensamblador AVR

ret;; Convert binary word in R2:R1 to a decimal digit by substracting; the decimal divider value in R4:R3 (1000, 100, 10);fpconv10d:

ldi rmp,'0' ; start with decimal value 0fpconv10d1:

cp R1,R3 ; Compare word with decimal divider valuecpc R2,R4brcc fpconv10d2 ; Carry clear, subtract divider valueret ; done subtraction

fpconv10d2:sub R1,R3 ; subtract divider valuesbc R2,R4inc rmp ; incrementar un dígitorjmp fpconv10d1 ; una vez más

;; End of floating point conversion routines;; End of conversion test routine

Planificar un proyecto en ensamblador AVR

Ahora se verán los conceptos básicos de como planificar un proyecto simple para programar en ensamblador. Debido a que lo que más determina es el componente hardware, primero se verá las consideraciones de hardware. A continuación use verán las interrupciones y después el tema de los tiempos.

Consideraciones de hardware

Para decidir que tipo de AVR se ajusta mejor a las necesidades hay que tener en cuenta una serie de consideraciones. He aquí las más relevantes:

1. ¿Que conexiones de puerto con ubicación fija se necesitan? Los puertos de I/O de los componentes internos sólo están disponibles en pines determinados y no se pueden cambiar. A continuación habría que tener en cuenta:

1. Si el procesador debe ser programable en circuito (interface ISP), tienen que asignarse para este propósito los pines SCK, MOSI y MISO. Hay que ver si el periférico permite que estos puedan ser utilizados como entradas (por ejemplo SCK y MOSI) o como salidas (por disociación a través de resistencias o multiplexores).

2. Si se necesita una interfaz serie, hay que reservar RXD y TXD para tal fin. Si se utiliza el protocolo de hardware de enlace RTS/CTS, son necesarios dos pines de puerto adicionales aunque se puede utilizar cualquiera que este libre.

3. Si se necesita un comparador analógico. AIN0 y AIN1 tendrían que ser reservados para ello.4. Si se necesita señales externas para monitorizar cambios de nivel, hay que reservar INT0 y/o

INT1.5. Si se van a utilizar convertidores AD, deben emplazarse y reservarse las entradas ADC. Si el

convertidor lleva conexiones AVCC y AREF externas se deben cablear en consecuencia.6. Si se van a contar pulsos externos, los pines de entrada de temporizador T0, T1 y T2 son fijos

y exclusivos para esto.7. Si hay que añadir SRAM externa, hay que reservar la respectiva dirección y los puertos de

datos junto con ALE, RD y WR.8. Si el reloj del procesador debe ser generado a partir de un oscilador de cristal externo, hay

que reservar XTAL1. Si se utiliza un cristal externo o un resonador cerámico para controlar la

64

Page 65: Ensamblador AVR

frecuencia de reloj, XTAL1 y XTAL2 han sido fijados para tal fin.

2. ¿Hay componentes externos que requieran más de un portpin (2, 4 u 8) para ser leidos o escritos? Éstos deben definirse de manera adecuada (en el mismo puerto, en el orden correcto).

1. Si para el control de un dispositivo externo se requiere la lectura o escritura de más de un bit a la vez, ej. una interfaz de pantalla LCD de cuatro u ocho bits, los bits de puerto necesarios deben estar en el orden correcto. Si no es posible colocar toda la interfaz en un solo puerto, la interfaz se puede dividir en dos partes y para el software es más fácil, si las partes se ajustan de izquierda a derecha.

2. Si se requieren dos o más canales ADC, para el software es más fácil si éstos se colocan en orden, por ejemplo, ADC2+ADC3+ADC4.

3. Al final, todos los componentes externos pueden estar colocados de forma que no requieran pines fijos.

1. Si por un simple pin hay que seleccionar un dispositivo más grande, puede considerar utilizar el pin de RESET para evitarlo. Éste puede ser usado si se establece determinado fusible. El ajuste de este fusible desactiva la programación ISP y el chip sólo se puede programar en el modo de alto voltaje. Esto es aceptable para producción final pero no para la creación de un prototipo. En el caso de crear un prototipo con una interfaz de programación de alto voltaje, el pin de ISP puede ser utilizado si el componente en el pin de RESET está protegido contra los 12 V durante la programación ISP, con una resistencia y un diodo zener.

Otras consideraciones para elegir con cuál procesador va a trabajar mejor pueden ser:

– Que temporizadores son necesarios y que resolución deben éstos proporcionar.– ¿Qué valores/datos hay que preservar cuando se apaga la alimentación? (Capacidad de

EEPROM)– ¿Cuánto espacio de almacenamiento se requiere? (Capacidad de SRAM)– ¿Qué requerimientos de espacio hay en el PCB? ¿Qué procesador encaja mejor y que tipos de

empaquetado hay disponibles?– Voltajes de operación y requisitos de energía. Si la tensión de alimentación proviene de una

batería o un acumulador. Las características de poder pueden jugar un papel importante.– Precio del dispositivo. Es relevante para una gran producción en serie. No todo depende del

procesador interno y las condiciones del mercado son impredecibles.– ¿Disponibilidad? Si uno comienza un proyecto con un AT90S1200 de un cajón de objetos usados

le puede salir muy barato, pero tal decisión no es muy sostenible. Trasladar un proyecto de un pequeño a un gran dispositivo o viceversa termina en un completo rediseño de software. Es mejor tenerlo en cuenta desde el principio porque así se vé y se trabaja mejor y es cuestión de una pequeña fracción de lineas de código.

Consideraciones sobre las interrupciones

Tareas muy simples funcionan bien sin las interrupciones. Si el consumo de energía es un problema, esto ya no es cierto. Casi todos los proyectos requieren interrupciones y esto debe ser planificado cuidadosamente.

Requisitos básicos de operación por interrupciones

Si no hay que tener en cuenta nada más he aquí los fundamentos:

- Habilitación de interrupciones:

– Las interrupciones requieren la pila de hardware. Así que hay que establecer SPL (y en dispositivos grandes SPH) al principio de RAMEND y reservar la parte superior de la

65

Page 66: Ensamblador AVR

SRAM para este propósito (por ejemplos los últimos 8 a x bytes).– Cada componente interno con su respectiva condición de provocación de una interrupción

se habilitan mediante el establecimiento del correspondiente bit de bandera de habilitación de interrupción. Los cambios de estos bits conllevan cierto riesgo por lo que sería mejor diseñar el software sin tener que cambiarlos.

– La bandera I en el registro de estado SREG tiene que establecerse al principio y permanecer establecida durante la operación. Si es necesario despejar la bandera I durante una operación fuera de la rutina de servicio de la interrupción, agregue la instrucción de establecimiento de la bandera I en unas pocas instrucciones.

- Tabla de vectores de interrupción:

– Cada componente interno con su respectiva condición de interrupción corresponden a un vector específico de interrupción situado en una determinada dirección de memoria flash de programa. La instrucción en esta dirección es la instrucción RJMP de una palabra o en los procesadores grandes Atmega, la instrucción JMP de dos palabras que salta a la respectiva rutina de servicio de interrupción.

– Las direcciones de los vectores son específicas del tipo de AVR. Al portar el software a un tipo diferente se requieren ajustes.

– A cada dirección de vector en la tabla que actualmente no es utilizada, se le da una instrucción RETI(en los tipos grandes Atmega, una RETI seguida de una NOP). Esto impide la ejecución de una interrupción fantasma por error. El uso de la directiva .ORG para ajustar las direcciones de vector no ofrece seguridad ante estos posibles eventos.

– Si se produce una condición de interrupción, se establece la respectiva bandera en el registro de control del componente interno. Ésta se pone a cero automáticamente si se ejecuta la interrupción. En algunos casos excepcionales (por ejemplo el caso de una condición de interrupción de una UART por un buffer TX vacío al que no se le va a enviar más caracteres) tiene que ser puesta a cero primero la bandera de habilitación de interrupción del componente y la bandera de condición de interrupción después.

– Si se da la condición de interrupción en más de un componente a la vez, la interrupción con la dirección más baja tiene preferencia.

- Rutinas de servicio de interrupción:

– Cada rutina de servicio de interrupción comienza guardando el registro de estado SREG en un registro reservado exclusivamente para tal fin, y termina con la restauración del registro de estado como estaba al producirse la interrupción. Debido a que una interrupción puede ocurrir en cualquier momento incluso cuando el procesador está ejecutando las instrucciones del bucle principal del programa, cualquier alteración del registro de estado puede causar un funcionamiento impredecible.

– Antes de saltar a la rutina de servicio, el procesador lleva el contador de la instrucción en curso a la pila. La interrupción y el respectivo salto a la rutina de servicio deshabilita otras interrupciones temporalmente poniendo a cero la bandera I en el registro de estado SREG. Cada rutina de servicio termina con la instrucción RETI que retorna de la pila la instrucción siguiente a la que se estaba ejecutando cuando ocurrió la int. y establece de nuevo la bandera I.

– Debido a que durante la ejecución de una rutina de servicio puede haber nuevas solicitudes de interrupción, incluso de mayor prioridad, cada rutina de servicio tiene que ser lo más breve posible y realizar las operaciones críticas de la tarea en el mínimo tiempo. Las operaciones de respuesta demasiado largas deben realizarse fuera de la rutina de servicio de interrupción.

– Como interrumpir una rutina de servicio de interrupción no puede suceder, todas las rutinas de servicio de interrupción pueden utilizar el mismo registro temporal.

- Interfaz de rutina de servicio de interrupción y bucle de programa principal:

66

Page 67: Ensamblador AVR

– La comunicación entre la rutina de servicio de int. y el bucle del programa principal se realiza a través de banderas individuales, que se establecen dentro de la rutina de servicio y se ponen a cero en el bucle del programa principal. Esto último se hace con una simple palabra de instrucción o deshabilitando temporalmente las interrupciones durante este paso para bloquear sebreescrituras erróneas de otras banderas que se han podido cambiar entre las tres fases de lectura-modificación-escritura.

– Los valores que proporcionan las rutinas de servicio de int. son puestos en los registros dedicados o en lugares específicos de SRAM. Cada cambio de esos valores (que se utilizan más tarde fuera de la rutina de servicio) tiene que ser comprobado por posible corrupción si en medio ocurre otra interrupción. Manejar un sólo byte es fácil, pero la entrega de dos o más bytes requiere un protocolo de transferencia (desactivar interrupciones durante la transferencia, establecer la bandera para evitar sobreescrituras, etc). Por ejemplo, la entrega de un valor del temporizador de 16 bits requiere primero la desactivación de las interrupciones, de lo contrario la lectura de un segundo byte no es correspondiente al primer byte leído si ha ocurrido una interrupción de por medio.

- Bucle de programa principal:

– En el bucle principal del programa el procesador se envía a dormir seleccionando el modo de espera “inactivo”. Cada interrupción despierta al procesador que salta a la rutina de servicio de int. correspondiente y después del retornar de la interrupción continúa la operación en el bucle principal. Tiene sentido comprobar que no se establecieron banderas dentro de la rutina de servicio y si se da el caso hay que realizar el tratamiento de la bandera. Después de realizar todas las operaciones se puede realizar otro chequeo de la configuración de las banderas (para rutinas muy largas) y el procesador se puede enviar de vuelta a dormir.

Ejemplo de un programa ensamblador por interrupciones

A continuación un programa de ensamblador por interrupciones que utiliza todas las reglas mencionadas anteriormente:

;; Definición de registros;

.EQU rsreg = R15 ; salvar el estado durante las interrupciones

.EQU rmp = R16 ; registro de las interrupciones temporales externo

.EQU rimp = R17 ; registro de las interrupciones temporales interno

.EQU rflg = R18 ; registro de bandera para la comunicación

.EQU bint0 = 0 ; bit de bandera para la señalización de servicio INT0

.EQU btc0 = 1 ; bit de bandera para la señalización de desbordamiento TC0; ...; Tabla ISR;.ORG $0000

rjmp main ; Vector de reset, ejecutado en el arranquerjmp isr_int0 ;vector INT0, ejecutado en los cambios de nivel en la linea de entrada INT0reti ; interrupción sin usoreti ; interrupción sin usorjmp isr_tc0_Overflow ; vector de desbordamiento TC0reti ; interrupción sin usoreti ; interrupción sin uso; ... otros vectores de int.

;; Rutinas de servicio de interupción;

67

Page 68: Ensamblador AVR

isr_int0: ; Rutina de servicio a para INT0in rsreg,SREG ; salvar estadoin rimp,PINB ; leer del puerto B el registro de temperaturaout PORTC,rimp ; escribir en el puerto C el registro de temperatura ; ... hacer otras cosassbr rflg,1<<bint0 ; señalización de INT0 fuera

out SREG,rsreg ; restaurar estadoreti ; retornar y habilitar las interrupciones

isr_tc0_Overflow: ; rutina de servicio de desbordamiento TC0in rsreg,SREG ; salvar estadoin rimp,PINB ; leer del puerto B el registro de temperaturaout PORTC,rimp ; escribir en elpuerto C el registro de temperatura; ... hacer otras cosassbr rflg,1<<btc0 ; establecer la bandera TC0out SREG,rsreg ; restaurar estadoreti ; retornar y habilitar las interrupciones

;; Inicio del programa principal;main:

ldi rmp,HIGH(RAMEND) ; establecer el registro de pilaout SPH,rmpldi rmp,LOW(RAMEND)out SPL,rmp; ... otras cosas a hacer; activar interrupción para desbordamiento TC0ldi rmp,1<<TOIE0 ; habilitación de interrupción de desbordamiento del temporizador 0out TIMSK,rmp ; establecer la mascara de desbordamiento del temporizadorldi rmp,(1<<CS00)|(1<<CS02) ; preescalador en 1024out TCCR0,rmp ; inicio del temporizador; habilitar interrupción en la entrada INT0ldi rmp,(1<<SE)|(1<<ISC00) ; habilitar modo inactivo e interrupción INT0 en todos los cambios de nivelout MCUCR,rmp ; al registro de controlldi rmp,1<<INT0 ; habilitar interrupciones INT0out GICR,rmp ; al registro de control de interrupción; establecer bandera de estado de interrupciónsei ; establecer bandera de interrupción

;; Bucle de programa principal;loop:

sleep ; procesador a dormirnop ; ficitcia para despertarsbrc rflg,bint0 ; no se establece bandera INT0rcall mache_int0 ; manejar el evento en INT0sbrc rflg,btc0 ; no se establece bandera de desbordamiento TC0rcall mache_tc0 ; manejar desbordamiento TC0rjmp loop ; volver a dormir

;; Manejar resultados de evento;mache_int0: ; manejar resultado INT0

cbr rflg,1<<bint0 ; despejar bandera INT0; ... hacer otras cosasret ; listo, retornar al bucle

mache_tc0: ; manejar desbordamiento TC0cbr rflg,1<<btc0 ; despejar bandera TC0; ... hacer otras cosasret ; listo, retornar al bucle

68

Page 69: Ensamblador AVR

Consideraciones de tiempo

Si un proyecto AVR va más allá de sondear un puerto I/O y en función de esto hacer algo, es necesario considerar el tema tiempo, que:

– Comienza con la selección del tipo de procesador.– Continúa con la pregunta de que es lo que ha de ser ejecutado periódicamente y con que

precisión.– Si hay posibilidades de control de sincronización y– cómo se pueden combinar las cosas.

Selección de la frecuencia de reloj del procesadores

La cuestión principal está en la precisión necesaria del reloj del procesador, aunque en aplicaciones que permitan un pequeño porcentaje de inexactitud, el oscilador interno RC de la mayoría de los AVR es suficiente. En los tipo tiny y mega se puede hacer una calibración del oscilador por lo que se reduce la diferencia entre la frecuencia nominal y la efectiva. Hay que tener en cuenta que en el byte de calibración interna por defecto fue seleccionada una tensión de servicio determinada. Si el voltaje de operación se fija en un nivel diferente, volver a escribir el byte de calibración aporta más precisión. Si el voltaje de funcionamiento es fluctuante, el error puede ser demasiado grande.

Si el reloj interno RC es demasiado lento o demasiado rápido, algunos tipos de dispositivo tienen un preescalador de reloj. Con esta característica pueden ser seleccionadas diferentes frecuencias de reloj y permite optimizar la frecuencia. Esto se hace de una vez cambiando un fusible de hardware (el fusible DIV8) o por software (por ejemplo para reducir el consumo de energía durante las pausas). Pero hay que tener en cuenta que algunos dispositivos con una especificación de reloj limitada (los de tipo V), no deben establecerse más allá de su límite o no volverán a funcionar correctamente.

Si el oscilador RC interno es demasiado impreciso, los fusibles se pueden configurar para un oscilador externo, un cristal (Xtal) o un dispositivo cerámico. Dado que una configuración errónea de los fusibles puede dar lugar a una catástrofe, una placa de rescate con un oscilador externo podría ser la última oportunidad para recuperar el dispositivo reseteando el fusible de nuevo.

La frecuencia de reloj absoluta debe ser la apropiada para la aplicación. Se puede utilizar como indicador la frecuencia que más a menudo realiza el paquete de trabajo. Si por ejemplo, tiene que ser consultada una tecla cada 2 ms, y esperar 20 veces este tiempo que debe ser lo suficientemente largo para que no haya efecto rebote, hay un montón de tiempo si se utiliza un reloj de 1 MHz (2.000 ciclos de reloj entre dos consultas, 20.000 para la ejecución repetida del comando de teclado).

Si hay que llegar a una frecuencia alta para una señal PWM (modulación por ancho de pulso) de alta resolución, como por ejemplo una frecuencia PWM de 10 KHz y 8 bits de resolución, 2.56 MHz son demasiado lentos para una solución basada en software. Mejor si un contador de tiempo con cierta sobrecarga de software lo puede asumir.

69