Mucho se ha escrito sobre macros en ensamblador. Y en la documentación, y en varios artículos. Pero en la mayoría de los casos, se reduce a una simple lista de directivas con una breve descripción de sus funciones, o a un conjunto de ejemplos dispares de macros preparadas.
El propósito de este artículo es describir un enfoque específico para la programación en lenguaje ensamblador para generar el código más simple y legible utilizando macros. El artículo no describirá la sintaxis de comandos y directivas individuales. El fabricante ya ha proporcionado una descripción detallada. Nos centraremos en cómo utilizar estas oportunidades para resolver problemas específicos.
En un momento, ATMEL probó y desarrolló una línea de microcontroladores de ocho bits con una arquitectura de muy alta calidad y un sistema de comando simple, pero al mismo tiempo muy potente. Pero, como saben, no hay límite para la perfección, y algunas instrucciones de uso frecuente no son suficientes. Afortunadamente, el ensamblador de macros, amable y absolutamente gratuito proporcionado por el fabricante, puede simplificar significativamente el código mediante el uso de directivas. Antes de pasar directamente a las macros, realizaremos algunos pasos preliminares.
Definición de constantes
.EQU FOSC = 16000000 .EQU CLK8 = 0
Estas dos definiciones le permiten deshacerse de los "números mágicos" en las macros, donde los valores de los registros se calculan en función de la frecuencia del procesador y el estado del fusible del divisor periférico. La primera definición es la frecuencia del cristal del procesador en hercios, la segunda es el estado del divisor de frecuencia periférico.
Registrar nombres
.DEF TempL = r16 .DEF TempH = r17 .DEF TempQL = r18 .DEF TempQH = r19 .DEF AL = r0 .DEF AH = r1 .DEF AQL = r2 .DEF AQH = r3
Un registro de nombres algo redundante a primera vista que se puede utilizar en macros. Solo se necesitan cuatro registros para Temp si se trata de valores de 32 bits (por ejemplo, en operaciones de multiplicar dos números de 16 bits). Si estamos seguros de que dos registros de almacenamiento temporal son suficientes para que los usemos en macros, entonces TempQL y TempQH no se pueden determinar. Las definiciones de A son necesarias para las macros que utilizan operaciones de multiplicación. AQ ya no es necesario si no usamos aritmética de 32 bits con nuestras macros.
Macros para implementar comandos simples
Ahora que hemos descubierto el nombre de los registros, comenzaremos a implementar los comandos que faltan y comenzaremos tratando de simplificar los existentes. El ensamblador AVR tiene una característica incómoda. Para entrada y salida, los primeros 64 puertos usan los comandos de entrada / salida , y para los lds / sts restantes. Para no mirar la documentación cada vez que se busca el comando necesario para un puerto específico, crearemos un conjunto de comandos universales que sustituirán independientemente los valores necesarios.
.MACRO XOUT .IF @0<64 out @0,@1 .ELSE sts @0,@1 .ENDIF .ENDMACRO .MACRO XIN .IF @1<64 in @0,@1 .ELSE lds @0,@1 .ENDIF .ENDMACRO
Para que la sustitución funcione correctamente, se utiliza la compilación condicional en la macro. En el caso de que la dirección del puerto sea inferior a 64, se ejecuta la primera sección condicional, de lo contrario la segunda. Nuestras macros repiten completamente la funcionalidad de los comandos estándar para trabajar con puertos de entrada / salida, por lo tanto, para indicar que nuestro equipo tiene características avanzadas, agregamos el prefijo de nombre estándar X.
Uno de los comandos más comunes que no están disponibles en el ensamblador, pero que se requieren constantemente, es el comando para escribir constantes en los registros de entrada de salida. La implementación de macro para este comando se verá así
.MACRO OUTI ldi TempL,@1 .IF @0<64 out @0, TempL .ELSE sts @0, TempL .ENDIF .ENDMACRO
En este caso, el nombre en la macro, para no violar la lógica de denominación de comandos, agregue al nombre estándar el postfix I , utilizado por el desarrollador para denotar los comandos para trabajar con constantes. En esta macro, usamos el registro TempL previamente definido para la operación .
En algunos casos, no se requiere un solo registro, sino un par completo que almacena un valor de 16 bits. Cree una nueva macro para escribir un valor de 16 bits en un par de registros de E / S
.MACRO OUTIW ldi TempL,HIGH(@1) .IF @0<64 out @0H, TempL .ELSE sts @0H, TempL .ENDIF ldi TempL,LOW(@1) .IF @0<64 out @0L, TempL .ELSE sts @0L, TempL .ENDIF .ENDMACRO
En esta macro, utilizamos las funciones LOW y HIGH incorporadas para extraer el byte bajo y alto de un valor de 16 bits. En el nombre de la macro, agregue los postfixes I y W al comando para indicar que en este caso el comando funciona con un valor de 16 bits (palabra).
No menos a menudo en los programas se cargan pares de registros, por ejemplo, para configurar punteros en la memoria. Creemos tal macro
.MACRO ldiw ldi @0L, LOW(@1) ldi @0H, HIGH(@1) .ENDMACRO
En esta macro, usamos el hecho de que la denominación estándar de registros y puertos en el fabricante implica el postfix L para el valor inferior y el postfix H para la parte superior del valor de doble byte. Si sigue esta regla al nombrar sus propias variables, la macro funcionará correctamente, incluso con ellas. La belleza de las macros también radica en el hecho de que proporcionan una sustitución simple, por lo tanto, en el caso de que el segundo operando sea un número, y en el caso de que este sea el nombre de la etiqueta, la macro funcionará correctamente.
Macros para implementar comandos complejos.
Cuando se trata de operaciones más complejas, generalmente no se usan macros, prefiriendo rutinas. Sin embargo, en estos casos, las macros pueden facilitar la vida y hacer que el código sea más legible. En este caso, la compilación condicional viene al rescate. Un enfoque de programación podría verse así:
Colocamos todas nuestras rutinas en un archivo separado, que llamaremos, por ejemplo, Library.inc . Cada subrutina en este archivo tendrá la siguiente forma
_sub0: .IFDEF __sub0 ; ----- ----- ret .ENDIF
En este caso, la presencia de la definición __sub0 significa que la subrutina debe incluirse en el código resultante. De lo contrario, se ignora.
A continuación, en un archivo separado Macro.inc, definimos macros del formulario
.MACRO SUB0 .IFNDEF __sub0 .DEF __sub0 .ENDIF ; --- call _sub0 .ENDMACRO
Al usar esta macro, verificamos la definición de __sub0 y, si falta, realizamos la determinación. Como resultado, el uso de una macro desbloquea la inclusión de código de subrutina en el archivo de salida. En el caso de usar rutinas en macros, el código del programa principal tomará la siguiente forma
.INCLUDE “Macro.inc” ;---- ---- .INCLUDE “Library.inc”
Como ejemplo, damos una implementación de una macro para dividir enteros sin signo de 8 bits. Mantenemos la lógica del fabricante y colocamos el resultado en AL (r0) , y el resto de la división en AH (r1) . La subrutina tendrá el siguiente aspecto
_div8u: .IFDEF __ div8u ;AH - ;AL ;TempL - ;TempH - ;TempQL - clr AL; clr AH; ldi TempQL,9 d8u_1: rol TempL dec TempQL brne d8u_2 ret d8u_2: rol A sub AH, TempH brcc d8u_3 add AH,TempH clc rjmp d8u_1 d8u_3: sec rjmp d8u_1 .ENDIF
La definición de macro para usar esta rutina será la siguiente
.MACRO DIV8U .IFNDEF __div8u .DEF __div8u .ENDIF mov TempL, @0 mov TempH, @1 call _div8u .ENDMACRO
Si lo desea, puede agregar una versión para trabajar con una constante
.MACRO DIV8UI .IFNDEF __div8u .DEF __div8u .ENDIF mov TempL, @0 ldi TempH, @1 call _div8u .ENDMACRO
Como resultado, el uso de la operación de división en el texto del programa es trivial
DIV8U r10, r11 ; r0 = r10/r11 r1 = r10 % r11 DIV8UI r10, 35 ; r0 = r10/35 r1 = r10 % 35
Mediante la compilación condicional, podemos ubicar todas las rutinas que podrían sernos útiles en Library.inc . En este caso, solo aquellos que fueron llamados al menos una vez aparecerán en el código de salida. Presta atención a la posición de la etiqueta de entrada. La salida de la etiqueta más allá de los límites de la condición se debe a las características del compilador. Si coloca la etiqueta en el cuerpo del bloque condicional, el compilador puede arrojar un error. La presencia de etiquetas no utilizadas en el código no da miedo, ya que la presencia de cualquier número de etiquetas no afecta el resultado.
Macros Periféricos
Una de las operaciones donde es difícil de hacer sin usar la documentación del fabricante es inicializar dispositivos periféricos. Incluso con el uso de designaciones mnemotécnicas de registros y bits del código, puede ser difícil entender en qué modo se configura un dispositivo en particular, especialmente porque a veces el modo se configura mediante una combinación de valores de bits de diferentes registros. Veamos cómo se pueden usar las macros con el ejemplo de USART .
Comencemos con la macro de inicialización de modo asíncrono.
.MACRO USART_INIT ; speed, bytes, parity, stop-bits .IF CLK8 == 0 .SET DIVIDER = FOSC/16/@0-1 .ELSE .SET DIVIDER = FOSC/128/@0-1 .ENDIF ; Set baud rate to UBRR0 outi UBRR0H, HIGH(DIVIDER) outi UBRR0L, LOW(DIVIDER) ; Enable receiver and transmitter .SET UCSR0B_ = (1<<RXEN0)|(1<<TXEN0) outi UCSR0B, UCSR0B_ .SET UCSR0C_ = 0 .IF @2 == 'E' .SET UCSR0C_ |= (1<<UPM01) .ENDIF .IF @2 == 'O' .SET UCSR0C_ |= (1<<UPM00) .ENDIF .IF @3== 2 .SET UCSR0C_ |= (1<<USBS0) .ENDIF .IF @1== 6 .SET UCSR0C_ |= (1<<UCSZ00) .ENDIF .IF @1== 7 .SET UCSR0C_ |= (1<<UCSZ01) .ENDIF .IF @1== 8 .SET UCSR0C_ = UCSR0C_ |(1<<UCSZ01)|(1<<UCSZ00) .ENDIF .IF @1== 9 .SET UCSR0C_ |= (1<<UCSZ02)|(1<<UCSZ01)|(1<<UCSZ00) .ENDIF ; Set frame format outi UCSR0C,UCSR0C_ .ENDMACRO
El uso de la macro nos permitió reemplazar la inicialización de los registros de configuración de USART con valores que eran incomprensibles sin leer la documentación por una línea que incluso aquellos que encontraron por primera vez este controlador podían manejar. En esta macro, también quedó claro por qué determinamos las constantes de frecuencia y divisor. Bueno, debe tenerse en cuenta que a pesar del impresionante código de la macro en sí, el resultante tendrá la misma apariencia que si estuviéramos escribiendo la inicialización de la manera habitual.
Para finalizar con USART, aquí hay algunas macros más pequeñas
.MACRO USART_SEND_ASYNC outi UDR0, @0 .ENDMACRO
Solo hay una línea, pero el uso de esta macro le permitirá ver mejor dónde el programa muestra los datos en USART . Si suponemos trabajar en modo síncrono sin usar interrupciones, en lugar de USART_SEND_ASYNC es mejor usar la macro a continuación
.MACRO USART_SEND USART_Transmit: xin TempL, UCSR0A sbrs TempL, UDRE0 rjmp USART_Transmit outi UDR0, @0 .ENDMACRO
En este caso, habilitamos la verificación de ocupación del puerto y mostramos datos solo cuando el puerto está libre. Obviamente, este enfoque para trabajar con dispositivos periféricos funcionará para cualquier dispositivo, y no solo para USART .
Comparación de programas sin y usando macros.
Veamos un pequeño ejemplo y comparemos el código escrito sin usar macros con el código donde se usan. Por ejemplo, tome un programa que muestre el clásico "¡Hola mundo!" a la terminal a través del hardware UART .
RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 USART_Init: out UBRR0H, r17 out UBRR0L, r16 ldi r16, (1<<RXEN0)|(1<<TXEN0) out UCSRnB,r16 ldi r16, (1<<USBS0)|(3<<UCSZ00) out UCSR0C,r16 ldi ZL, LOW(STR<<1) ldi ZH, HIGH(STR<<1) LOOP: lpm r16, Z+ or r16,r16 breq END USART_Transmit: in r17, UCSR0A sbrs r17, UDRE0 rjmp USART_Transmit out UDR0,r16 rjmp LOOP END: rjmp END STR: .DB “Hello world!”,0
Y aquí está el mismo programa, pero escrito usando macros
.INCLUDE “macro.inc” .EQU FOSC = 16000000 .EQU CLK8 = 0 RESET: ldiw SP, RAMEND; USART_INIT 19200, 8, "N", 1 ldiw Z, STR<<1 LOOP: lpm TempL, Z+ test TempL breq END USART_SEND TempL rjmp LOOP END: rjmp END STR: .DB “Hello world!”,0
En este ejemplo, utilizamos las macros descritas anteriormente, lo que nos permitió simplificar significativamente el código del programa y hacerlo más comprensible. El código binario en ambos programas será absolutamente idéntico.
Conclusión
El uso de macros puede reducir significativamente el código de ensamblador del programa, para hacerlo más comprensible y legible. La compilación condicional le permite crear comandos universales y bibliotecas de procedimientos sin crear código de salida redundante. Como inconveniente, se puede señalar un conjunto muy modesto según los estándares de lenguajes de alto nivel de operaciones y restricciones permitidas al declarar datos "hacia adelante". Esta restricción no permite, por ejemplo, escribir por medio de macros un comando universal completo para las transiciones jmp / rjmp e infla significativamente el código de la macro al implementar una lógica compleja.