A lot has been written about macros in assembler. And in the documentation, and in various articles. But in most cases, it comes down either to a simple listing of directives with a brief description of their functions, or to a set of disparate examples of ready-made macros.
The purpose of this article is to describe a specific approach to assembly language programming to generate the most simple and readable code using macros. The article will not describe the syntax of individual commands and directives. A detailed description has already been given by the manufacturer . We will focus on how to use these opportunities to solve specific problems.
At one time, ATMEL tried and developed a line of eight-bit microcontrollers with a very high-quality architecture and a simple, but at the same time very powerful command system. But, as you know, there is no limit to perfection, and some frequently used instructions are not enough. Fortunately, the macro assembler, kindly and absolutely free of charge provided by the manufacturer, can significantly simplify the code through the use of directives. Before proceeding directly to the macros, we will perform some preliminary steps
.EQU FOSC = 16000000 .EQU CLK8 = 0
These two definitions allow you to get rid of "magic numbers" in macros, where the values ​​of the registers are calculated based on the processor frequency and the state of the fuse of the peripheral divider. The first definition is the frequency of the processor crystal in hertz, the second is the state of the peripheral frequency divider.
.DEF TempL = r16 .DEF TempH = r17 .DEF TempQL = r18 .DEF TempQH = r19 .DEF AL = r0 .DEF AH = r1 .DEF AQL = r2 .DEF AQH = r3
A somewhat redundant at first glance naming registers that can be used in macros. Just four registers for Temp are needed if we are dealing with 32-bit values ​​(for example, in operations of multiplying two 16-bit numbers). If we are sure that two temporary holding registers are enough for us to use in macros, then TempQL and TempQH can not be determined. Definitions for A are needed for macros using multiplication operations. The need for AQ disappears if we do not use 32-bit arithmetic with our macros.
Now that we’ve figured out the naming of the registers, we’ll start implementing the commands that are missing and start by trying to simplify the existing ones. The AVR assembler has one awkward feature. For input and output, the first 64 ports use the in / out commands, and for the remaining lds / sts . In order not to look at the documentation every time in search of the necessary command for a specific port, we will create a set of universal commands that will independently substitute the necessary values.
.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
In order for the substitution to work correctly, conditional compilation is used in the macro. In the case when the port address is less than 64, the first conditional section is executed, otherwise the second. Our macros completely repeat the functionality of standard commands for working with input / output ports, therefore, to indicate that our team has advanced capabilities, we add the standard naming prefix X.
One of the most common commands that are not available in assembler, but are constantly required, is the command to write constants to the output input registers. The macro implementation for this command will look like this
.MACRO OUTI ldi TempL,@1 .IF @0<64 out @0, TempL .ELSE sts @0, TempL .ENDIF .ENDMACRO
In this case, the name in the macro, so as not to violate the command naming logic, add to the standard name the postfix I , used by the developer to denote the commands for working with constants. In this macro, we use the previously defined TempL register for operation .
In some cases, not a single register is required, but a whole pair that stores a 16-bit value. Create a new macro to write a 16-bit value to a pair of I / O registers
.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
In this macro, we use the built-in LOW and HIGH functions to extract the low and high byte from a 16-bit value. In the macro name, add the postfixes I and W to the command to indicate that in this case the command works with a 16-bit value (word).
No less often in programs there is loading register pairs, for example, for setting pointers to memory. Let's create such a macro
.MACRO ldiw ldi @0L, LOW(@1) ldi @0H, HIGH(@1) .ENDMACRO
In this macro, we use the fact that the standard naming of registers and ports at the manufacturer implies the postfix L for the lower and the postfix H for the upper part of the double-byte value. If you follow this rule when naming your own variables, then the macro will work correctly, including with them. The beauty of macros also lies in the fact that they provide a simple substitution, therefore, in the case when the second operand is a number, and in the case when this is the name of the label, the macro will work correctly.
When it comes to more complex operations, macros are generally not used, preferring routines. However, in these cases, macros can make life easier and make the code more readable. In this case, conditional compilation comes to the rescue. A programming approach might look like this:
We place all our routines in a separate file, which we will name, for example, Library.inc . Each subroutine in this file will have the following form
_sub0: .IFDEF __sub0 ; ----- ----- ret .ENDIF
In this case, the presence of the __sub0 definition means that the subroutine must be included in the resulting code. Otherwise, it is ignored.
Next, in a separate file Macro.inc we define macros of the form
.MACRO SUB0 .IFNDEF __sub0 .DEF __sub0 .ENDIF ; --- call _sub0 .ENDMACRO
When using this macro, we check for the existence of the __sub0 definition and, if it is missing, we perform the determination. As a result, using a macro unlocks the inclusion of subroutine code in the output file. In the case of using routines in macros, the code of the main program will take the following form
.INCLUDE “Macro.inc” ;---- ---- .INCLUDE “Library.inc”
As an example, we give an implementation of a macro for dividing 8-bit unsigned integers. We preserve the logic of the manufacturer and place the result in AL (r0) , and the remainder of the division in AH (r1) . The subroutine will look as follows
_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
The macro definition for using this routine will be as follows
.MACRO DIV8U .IFNDEF __div8u .DEF __div8u .ENDIF mov TempL, @0 mov TempH, @1 call _div8u .ENDMACRO
If desired, you can add a version for working with a constant
.MACRO DIV8UI .IFNDEF __div8u .DEF __div8u .ENDIF mov TempL, @0 ldi TempH, @1 call _div8u .ENDMACRO
As a result, using the division operation in the program text is trivial
DIV8U r10, r11 ; r0 = r10/r11 r1 = r10 % r11 DIV8UI r10, 35 ; r0 = r10/35 r1 = r10 % 35
Using conditional compilation, we can place in Library.inc all the routines that could be useful to us. In this case, only those that were called at least once will appear in the output code. Pay attention to the position of the entry label. The output of the label beyond the bounds of the condition is due to the features of the compiler. If you place the label in the body of the conditional block, the compiler may throw an error. The presence of unused tags in the code is not scary, since the presence of any number of tags does not affect the result.
One of the operations where it is difficult to do without using the manufacturer’s documentation is to initialize peripheral devices. Even with the use of mnemonic designations of registers and bits from the code, it can be difficult to understand in which mode a device is configured, especially since sometimes the mode is configured by a combination of bit values ​​of different registers. Let's see how you can use macros using the USART example.
Let's start with the asynchronous mode initialization macro.
.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
Using the macro allowed us to replace the initialization of the USART setup registers with values ​​that were incomprehensible without reading the documentation by a line that even those who first encountered this controller could handle. In this macro, it also finally became clear why we determined the frequency and divisor constants. Well, it should be noted that despite the impressive code of the macro itself, the resulting one will look the same as if we were writing initialization in the usual way.
To finish with USART, here are a few more small macros
.MACRO USART_SEND_ASYNC outi UDR0, @0 .ENDMACRO
There is only one line, but using this macro will allow you to better see where the program displays data in USART . If we assume to work in synchronous mode without using interrupts, then instead of USART_SEND_ASYNC it is better to use the macro below
.MACRO USART_SEND USART_Transmit: xin TempL, UCSR0A sbrs TempL, UDRE0 rjmp USART_Transmit outi UDR0, @0 .ENDMACRO
In this case, we enable port occupancy checking and display data only when the port is free. Obviously, this approach to working with peripheral devices will work for any device, and not just for USART .
Let's look at a small example and compare the code written without using macros with the code where they are used. For example, take a program that displays the classic "Hello world!" In the terminal through a 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
And here is the same program, but written using 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
In this example, we used the macros described above, which allowed us to significantly simplify the program code and make it more understandable. The binary code in both programs will be absolutely identical.
Using macros allows you to significantly reduce the assembler code of the program, to make it more understandable and readable. Conditional compilation allows you to create universal commands and libraries of procedures without creating redundant output code. As a drawback, one can point out a very modest by the standards of high-level languages ​​set of permissible operations and restrictions when declaring data “forward”. This restriction does not allow, for example, to write a full-fledged universal command for jmp / rjmp transitions using macro tools and significantly inflates the code of the macro itself when implementing complex logic.