• 【深入理解计算机系统】 八: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..

  • 相关阅读:
    摊牌了……开始入坑硬件开发……Arduion点亮oled小屏
    最后的晚餐——dubbo其他剩余高级知识点分享
    dubbo的负载均衡以及配置方式补充
    dubbo知识点之管理工具dubbo-admin分享
    could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable',
    netcore 后台任务 指定每天某一时间执行任务
    C# 线程Timer的Change
    EF 取值时出错: Specified cast is not valid
    C# 比较两个数据的不同
    c# json数据解析——将字符串json格式数据转换成对象或实体类
  • 原文地址:https://www.cnblogs.com/geeksongs/p/14118662.html
Copyright © 2020-2023  润新知