【深入理解计算机系统】 八:AVR汇编语言

8.1. Creating an Executable Program from Assembly Code

The following figure shows an assembly program. You may create this program using a plain text editor, that is, a program that stores the text you see in the screen as a simple ASCII or UNICODE sequence of characters, with no information about style.

 

Example of Assembly Program

An assembly program is divided into sections each of them delimited by a sentence marking its start. The line .section .data does not translate into any instruction, but instead is a mark to let the assembler know that the text appearing after contains data definitions. The words included in the program that are not translated into code but instead are order for the assembler are called directives.

In its second line, the program defines a string which is encoded as a sequence of ASCII values. The line contains the directive .asciz followed by the string surrounded by double quotes which instructs the assembler to allocate space in memory and store the string in a specific location. This definition of a string is preceded by the word my_msg followed by a colon. This is the way a label is defined in an assembly code. The name of the label has been chosen by the programmer as the way to refer to the string defined in that line. As you can see, this construction could be thought as the equivalent of declaring and defining a variable in a high level programming language, but in a much more simplified way.

The line containing the string .section .text tells the assembler that the data definitions have finished and the following lines contain the assembly instructions (the code).

The following line contains one more directive declaring the word main as a global symbol. This means that the location in the code with such label can be accessed from outside of the file. This is the mechanism used in assembly code to define the starting point of the program. By convention, the program will start in the instruction labelled with the name main and such label must be declared global.

Once this program has been created with the editor in a file with name file.s, the assembler is invoked with the following command:

avr-gcc -Wall -g -Os -DF_CPU=16000000UL -mmcu=atmega328p -o program file.s

and the executable with name program is created. The assembler performs a set of steps similar to what a Java compiler does. If there is any syntactical error in the program, a message is shown in the screen and the program is not created. If the program is correctly written, the command finished with no message and the program has been created.

Any assembly program must then have following structureLinks to an external site.:

;;; Data definitions go here
.section .data

;;; Code definition goes here
.section .text
        .global main

main:
        ret
.end

Remarks in the code can be included either with the prefix ;; at the beginning of the line, or simply with ; in the middle of a line. The characters remaining in the line will be ignored by the assembler.

Based in this template, the program shown in the previous figure has executed the instructions:

push R24
push R25

ldi R24, lo8(my_msg)
ldi R25, hi8(my_msg)
push R25
push R24
call printf
pop R0
pop R0

pop R25
pop R24
ret

The first two instructions are copying the content of two registers in the stack. This is because the program will finish with all registers containing the values that had at the beginning of the execution (except for R0), a convention that we will implement in all our programs. The instructions ldi load specific values in registers R24 and R25 which are then uploaded in the stack. The subroutine printf prints a message in the serial monitor and expects the address of that string in the stack.

The instructions pop R0 simply remove the values previously loaded in the stack so that it is restored to its initial state. The last two instruction restore the initial values of registers R25 and R24 respectively.

The following video shows how an AVR assembly program is created..

 

8.2. Data Definition

Assembly programs, as other programming languages, allow the definition of data structures with its content. However, this language is so close to the representation used by the microprocessor, that only simple data structures are allowed. The complex data structures offered by high level programming languages such as Java are translated in terms of simple memory definitions by the compiler.

All data definitions in an assembly program must be included in the data section which starts after the directive .section .data. All the data defined in the section is stored in consecutive memory locations. If two variables are defined in contiguous lines, they will be stored next to each other in memory.

The main difficulty to manipulate data in assembly is that there is no information stored in memory to remember the type of data or its structure. At the level of machine level the microprocessor is simply manipulating bytes, with no notion whatsoever of the structures the programmer intended to define. No type checking of any sort is done, as no type information is retained. In other words, an assembly program may access the bytes that encode the letters of a string and treat them as integers, naturals, floating point or any other available format. The data manipulated by any instruction is only referred by its address.

8.2.1. Byte and Integer Definition

The definition of numeric values of size one byte is done using the directive .byte followed by one or several values separated by commas. Upon staring the program, these bytes are properly stored in consecutive locations starting at a specific address. You may access such address by first defining a label at the beginning of the line. The directive also allows the definition of byte values using different syntax as shown in the following example:

data:    .byte 38, 0b11011110, 0xFF, 'A', 0344, -12

The previous directive is translated into the following layout in memory (assuming 1 byte cells):

The programmer has no control over the address in which the data is stored. In the picture the value 0x120 has been chosen arbitrarily.

If a number larger than 255 is given, the assembler flags it as an error. If a negative integer is given that cannot be represented accurately in a byte, the number is truncated and a warning message is printed during the compilation process.

Review Questions: Byte and Integer Definition

8.2.2. String Definition

The definition of strings can be done with two different formats using three directives. The first one is .asciiand must be followed by a list of strings each delimited by double quotes and separated by commas. Each character in the string is encoded with one byte using the ASCII encoding using consecutive memory positions.

The .asciz directive is identical to the previous one, it allows a list of double-quote delimited strings separated by commas, but each string is encoded with an extra byte with value 0x00 as last character. The directive.string is a synonym of .asciz.

 

Use of String Directives and its Memory Representation

The first and second string in the previous figure occupy each three positions. The following two strings each occupy four positions because of the byte with value 0x00 added at the end of the encoding. The last string is also encoded with the additional byte at the end.

Review Questions: String Definition

8.2.3. Empty Space Definition

The .space directive followed by two numbers separated by a comma allows to reserve memory space with as many bytes as the first value and initialized with the second value. If the second number is omitted, the memory is initialized with the value zero. The following figure shows an example of this directive and its effect in memory:

8.3. Labels

A program written in assembly code has a data section containing the data, and a code section containing the instructions to execute. Before the microprocessor starts executing the program, though, both code and data must be placed at a specific location in memory. But, if a program has to manipulate data, it must be able to process its addresses, and how are the addresses known when writing the program?

The mechanism offered by the assembler to manage address values are labels. Labels are strings that are written at the very beginning of a line in the program and are followed by a colon. The name of the label is used by the assembler as a reference to the memory address of that location in the program. Labels can be used for both data and code addresses. The addresses represented by the labels can be used to access not only the exact data they point to, but also other data with address derived from processing the label with arithmetic operations. Consider the definition of bytes shown in the following figure:

 

Label to Reference Memory Addresses

The .byte directive places as many values in consecutive byte positions in memory. The label data represents the address of the first of these values. Knowing the size of the data, we can derive the position of the rest of the elements by simply adding its offset from the first position. Remember that in assembly code there is no notion of data structures, thus, all the bytes can be accessed with no restriction (as long as they are part of the data section in the program).

Labels can be included directly as operands in instructions to refer to the data they point to. For example, the instruction

LDS R12, data

would load the byte with value 0x26 defined as shown in the previous figure in register R12. Even though we do not know the address where these numbers will be stored, the definition of a label and its use in an instruction allows us to manage the data with no problem. Thus, the following instruction

STS data, R12

stores the content of register R12 in the memory location in which data has been defined.

Whenever a label is found as operand of an instruction the assembler marks that instruction as needing a review. When the program is loaded in memory and is about to execute, the address of the label in that instruction is then replaced with the actual location in which the data is stored. This technique allows users to write assembly programs knowing only the relative position of data with respect to labels.

Labels must be unique throughout the entire file containing the assembly code and can be used indistinctly in the data or code part. Labels in the code are used as operands of subroutines or conditional branches.

But labels as instruction parameters are only useful to refer to the memory location where the label is defined. If we want to use the label to access memory at a certain distance from the label, arithmetic operations are needed. If this is the case, how is the address obtained? How can the address of any data be loaded in a register for its processing?

This problem is solved by the assembler by providing two additional directives called hi8() and lo8(). If label has been previously defined in any location in the code, the expression hi8(label) returns the 8 most significant bits of the address assigned to the label. Analogously, the expression lo8(label) returns the 8 least significant bits of the address assigned to the label.

In the AVR architecture, memory addresses have 16 bits and the general purpose registers have 8 bits, this is the reason why two functions (hi8 and lo8) are given instead of a single function returning the entire address. The use of these functions in the code is shown by the following example:

.section .data

data: .byte 0x03, 0x04

...

.section text
.globl main

        ...
        LDI R28, lo8(data)
        LDI R29, hi8(data)

After executing these instructions, the address represented by data is loaded in the 16 bits made out by concatenating the 8-bit registers R29:R28. Remember that these two registers when concatenated make the 16 bit register that the architecture allows us to manipulate with the name Y and the instruction LDD allows to specify a constant to be added to the register in the second operand. Thus, once the address of the label has been pre-loaded in these two registers the following sequence of instructions

LD R11, Y
LDD R12, Y + 1
LDD R13, Y + 2
LDD R14, Y + 3
LDD R15, Y + 4
LDD R16, Y + 5

Load the five bytes previously defined into the appropriate registers. And symmetrically, the following instructions

ST Y, R11
STD Y + 1, R12
STD Y + 2, R13
STD Y + 3, R14
STD Y + 4, R15
STD Y + 5, R16

store the values in the registers back to the initial positions. These two sequence of instructions would be equivalent to

LD R11, Y+
LD R12, Y+
LD R13, Y+
LD R14, Y+
LD R15, Y+
LD R16, Y+
...
ST Y+, R11
ST Y+, R12
ST Y+, R13
ST Y+, R14
ST Y+, R15
ST Y+, R16

with the exception that in the second sequence, register Y is increased by each instruction, whereas in the previous sequences, the register is left untouched.

These instructions and its possible operands are an example of how microprocessors allow some basic arithmetic operations over addresses (contained in registers) to be specified in the instruction’s parameter. The type of operations allowed in the parameters is different for each microprocessor and is one of the defining features of the architecture.

Aside from these operations included as part of the operands, once the value of an address is loaded in a register, it can be manipulated by any of the instructions available as any other numeric operand.

The following sequence of instructions shows the use of labels in the code portion of an assembly code. In this case, they are only used to mark certain location in the code and then refer to it to jump or make a function call.

loop:
        cpi R12, 3
        breq end_of_loop ;; Conditional branch to label end_of_loop
        ...
        ...
        ...
        jmp loop         ;; Jump to label loop

end_of_loop:             ;; Label definition
        add R1, R2
        ...
        ...
        call function    ;; Call subroutine in location function
        ...
        ...

function:
        push R12 ;; First instruction in function
        ...
        ...
        ret

Labels can be considered as the basic mechanism used by compilers to declare variables. Each variable in a program has a memory location which could be assimilated to the address of the label. However, high level programming languages place additional information in a variable such as its type that is used for multiple checks that are not possible in assembly programs.

8.4. Stack and Register Management

Writing assembly code has numerous restrictions that appear because the program will run almost directly on the microprocessor. Handling the stack is one of these restrictions that is present at assembly level but is totally hidden when programming a digital system in a high level programming such as Java.

8.4.1. Stack Restrictions

The Stack is used as a temporary repository of data while the program is executing. The instructions push and pop are used to place and remove data in the stack, but are not the only ones that modify its content. The processor uses the stack to store additional data, which means, assembly programs have a few restrictions on how the stack must be used for a program to execute correctly.

The most important restriction imposed over the stack is that the top of the stack (the data pointed by the stack pointer) must be exactly the same before the first instruction of a routine is executed, and before the execution of the last instruction (which is always the instruction RET). This means that even though there is a stack available when an assembly program is started and it can be used to store temporary values, its content must be restored to its initial state before the end of the program. The main consequence of this restriction is that a subroutine will place on the stack some values that are used during the execution, and then remove all of them from the stack before the end of the subroutine.

The operating system running in a microprocessor is the entity in charge of preparing the executing environment for a program, and the stack is part of such environment. A portion of memory is reserved for the stack and the stack pointer is initialized with its address. Once the execution environment is ready, the routine with name main is then invoked, marking the start of the execution.

8.4.2. Register Restrictions

The register file is also subject to some arbitrary restrictions. Some of them are derived from the architecture, but some other may be a convention so that designers can create programs that can be called by other programs. This is specially the case when it comes to register use.

In the AVR architecture, and more precisely, in the code generated by the compiler avr-gcc that uses the AVR libc library, the following convention for register management is assumed:

  1. Register R0 is used as scratch register and need not to be saved nor restored.
  2. Register R1 must be kept always at value zero.
  3. Registers R18 to R27R30 and R31 may not be saved before its use, but they may be overwritten when a function is called from your code.
  4. Registers R2 to R17R28 and R29 must be saved and restored at the beginning and end of a subroutine.
  5. If a function returns an integer as result, this must be placed in register R25:R24 at the end of the subroutine. The calling subroutine will expect that result to be present in those registers.

The following figure illustrates this policy with a colouring code:

Register Usage Policy in AVR Assembly Programs

The following sequence of instructions shows a function that complies with this restriction:

asm_function:
        push R3          ;; Saving all the registers used in the function
        push R4
        push R16
        push R28
        push R29

        lds R16, s       ;; Instruction modifies R16

        ldi R28, lo8(t)  ;; Instructions modifies R29:R28
        ldi R29, hi8(t)

        lds R3, m        ;; Instruction modifies R3
        cpi R16, 0
        breq done

        dec R16
        ld R4, Y+        ;; Instruction modifies R4

        ...
        ...

        pop R29         ;; Restoring all the registers used in the function
        pop R28
        pop R16
        pop R4
        pop R3
        ret

Since the registers are stored in the stack, the order of the push instructions at the beginning of the subroutine is symmetric to the order of the pop instructions before the end of the subroutine.

The following video shows how to declare data, use labels, the stack and register in an AVR assembly program..

 

8.5. Guidelines for Assembly Programming

Writing assembly programs is a task that requires a detailed knowledge of the architecture of a microprocessor and an extremely meticulous use of instructions, operands and data. Error checking during the execution of assembly programs is virtually non-existent. In other words, if a program does not exhibit the expected behaviour, the anomaly needs to be found mostly by carefully reviewing the sequence of instructions.

Although, as with any other programming language, there is no set of rules that guarantee that an assembly program is designed correctly, there are several recommendations that may help reduce the time to write a program. We strongly encourage you to observe them as they are proven to simplify the tasks:

  • The values in registers R1 to R17R28 and R29 must be identical to those that were present before executing your program (or subroutine).
  • Avoid unnecessary operations. For example, do not save and restore registers that are not needed or the content of which is not important. Try to move the data to the right location to begin with to avoid unnecessary data movement operations.
  • There is always more than one way to program a task. If possible, choose the one with less number of instructions or that you think it will execute faster. Memory accesses typically slow down program execution.
  • Write legible code. Insert comments before blocks of instructions so that you know what is the purpose of several instructions. Do not include trivial comments about a single instruction, but instead, high level comments about code blocks.

8.6. Example of Assembly Program

The following is an example of an assembly program that adds the content of four numbers stored in memory and stores its result in a reserved location.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
;; Data definitions go here
.section .data
n1:     .byte 12
n2:     .byte 34
n3:     .byte 21
n4:     .byte 10
result: .space 1

;; Code definition goes here
.section .text
	.global main

main:
	push R17
	
	ldi R26, lo8(n1)        ; Loading the address of n1 in X
        ldi R27, hi8(n1)

        ld R24, X+              ; Load n1 in R24 and increase address
        ld R17, X+              ; Load n2 in R17 and increase address
        add R24, R17            ; R4 = n1 + n2
        ld R17, X+              ; Load n3 in R17 and increase address
        add R24, R17            ; R4 = n1 + n2 + n3
        ld R17, X+              ; Load n4 in R17 and increase address
        add R24, R17            ; Final result in R24 = n1 + n2 + n3 + n4

        ;; At this point R27:R26 contains the address of result
        st X, R24               ; Store the result

	;; The result of this function is returned in R25:R24
        clr R25			; So that R25:R24 has the result in 16 bits.

	pop R17
	ret
.end

The data section contains the definition of the four 8 bit integers in consecutive locations followed by the space to store the result. Each integer is defined in a single line with its own label, but such structure is arbitrary. An equivalent definition of the same data structure would be

.section .data
numbers:
        .byte 12, 34, 21, 10, 0

The fact that the numbers are stored in consecutive locations can be used to access them through a single label, which represents the address of the first one, and then add the appropriate values to add the other locations. This is in fact what the code does. As it can be seen, the address of label n1 is first obtained, and then manipulated to add the rest of the numbers.

The code uses five registers:
  • R17 for temporary storage of the values that are loaded from memory.
  • R24 to accumulate the result of the addition.
  • R25 to return the result of the function as R25:R24.
  • R27:R26 or X to store the address of the data being accessed.

The Register Restrictions state that out of these five registers, only R17 must be saved. The rest can be used freely. This is the reason why the code starts with the instruction push R17 and finishes with the instruction pop R17 before the instruction ret. These two instructions guarantee that the content of that register is left untouched despite of it being used in the middle of the calculations.

The following two instructions illustrate how to get the address represented by a label in the data section:

ldi R26, lo8(n1)
ldi R27, hi8(n1)

Memory address have 16 bits, but the operations on registers only allow to load 8 bits at a time. The solution comes with some help from the assembler. The functions lo8() and hi8() are directives for the assembler to replace them with the lower or higher 8 bits respectively of the address represented by the label given as parameter. These two functions are simply an abbreviation that is replaced by the proper number during the translation process, they do not represent any additional computation done by the program.

The following instruction shows how to access a memory location using an address loaded in the register, and at the same time, increase the value on that register:

ld R24, X+

The data stored in memory in the address contained in register X (that is R27:R26) is loaded into register R24In the same instruction, register X is increased by one unit. This instruction uses what is known as an indirect addressing mode which will be fully described in another chapter.

Two things are then achieved by this instruction. The first data is loaded from memory into a register ready to be processed by other instructions, and register X is left pointing to the address of the next number. Increasing register X could have been done with two auxiliary instructions, but the since the processor allows this increase to occur in the same instruction, it should be used. In general, assembly programmers must be aware of these type of features provided by a microprocessor so that the code is written efficiently and the execution time is reduced.

The following instructions repeat the steps of loading a number from memory and accumulating its sum in register R24. The instruction

st X, R24

stores the result of the sum in the location reserved for such purpose. Note that register X is used again because in the previous access to memory it was used to load the number in location n4 and its value was increased. There is no need to increase the value of the register in the store instruction as it will not be used to access any other data.

Finally, the function returns a 16 bit integer as a result, which the register restrictions state that must be stored in register R25:R24.

The instruction clr R25 sets the register to zero. This is because the program can return a 16 bit number, and this can be done using the concatenation of registers R25:R24. The last two instructions are to restore the value of R17 previously stored in the stack, restore the stack to its initial configuration, and finish the execution of the program.

The following video shows how to write a program that adds four numbers in AVR assembly..

posted @ 2020-12-11 09:20  Geeksongs  阅读(307)  评论(0编辑  收藏  举报

Coded by Geeksongs on Linux

All rights reserved, no one is allowed to pirate or use the document for other purposes.