65.9K
CodeProject 正在变化。 阅读更多。
Home

AVR 汇编入门 101

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (35投票s)

2014年1月19日

CPOL

10分钟阅读

viewsIcon

138865

downloadIcon

2749

学习 AVR 微控制器和汇编语言的基础知识

可供下载的 AVR 汇编器画笔 shBrushAsm.js.zip 是一个脚本,与 Alex Gorbatchev 的 Syntaxhighlighter 脚本结合使用。它尚未经过全面测试,因此如果您发现任何问题,请告诉我。

为了学习汇编语言编程,我们必须了解我们正在使用的硬件。在本教程中,我们将首先简要介绍 AVR 微控制器的工作原理,然后转到纯汇编器,最后展示如何混合使用“C”和汇编语言。

  1. 引言
  2. 内存配置
  3. 访问内存
    1. 程序内存
    2. 数据内存
      1. 直接数据
      2. 带位移的间接数据
      3. 间接数据
      4. 带预减量的间接数据
      5. 带后增量的间接数据
    3. EEPROM 内存
  4. 输入/输出
  5. 语言
    1. 寄存器使用
  6. 混合语言
    1. 从 C 调用汇编子程序
    2. 从汇编器调用 C 子程序
  7. 完整的汇编程序示例
  8. 结论
  9. 参考文献

引言

既然有 C、C++ 等提供抽象层的语言可以消除编程的所有繁琐工作,为什么还会有人想用汇编器这样的低级语言进行编程呢?

  1. 有些人是受虐狂。
  2. 高级编译器生成的代码无法适应 MPU 的内存空间。
  3. 我们需要速度。
  4. 我们是控制狂,需要控制应用程序流程的每个方面。

学习汇编器还有另一个很好的理由,您对处理器内部工作原理了解得越多,您就会成为一个越有能力的程序员。即使您确实决定需要用汇编器编写部分代码,您也不局限于只使用汇编器或高级语言,只要我们遵守一些简单的规则,我们就可以将它们混合使用。例如,我们可以将“C”作为我们的主要语言,但用汇编器编写中断例程。

内存配置

AVR 采用哈佛架构,这是一种程序和数据拥有独立内存和总线的架构。程序内存中的指令以单级流水线执行。这意味着在一条指令执行时,下一条指令会从程序内存中预取。这个概念使得大多数指令可以在每个时钟周期执行。下图说明了典型 AVR 设备的内存映射。实际的内存配置将取决于所使用的特定 MPU,请查阅数据手册。

访问内存

由于 AVR 架构的脱节性质,每个内存段都需要以不同的方式访问。提供了用于程序和数据内存访问的指令,并且可以像在大多数其他处理器中访问内存一样检索或写入内存。EEPROM 访问将在其自己的部分中介绍,因为它在 Atmel 和大多数其他微控制器中是一种不同的“野兽”。

程序内存

程序内存应被视为只读内存,因此只有两条指令用于处理它:加载程序内存 (LPM) 和存储程序内存 (SPM),除非您正在编写自修改代码,否则实际上没有必要写入程序内存。

数据内存

在本节中,我们将介绍许多专门用于处理数据内存的指令。这些信息取自 ATMega1280 数据手册,由于并非所有处理器都以相同的方式使用这些指令,因此您需要查阅您正在使用的特定处理器的数据手册,以确保您正在使用的微控制器上提供了这些指令。

本节列出的指令仅限于 64K 数据段,有些甚至更少,但对于具有更大数据空间的处理器,有一个特殊寄存器 RAMPD,可以与一些间接指令结合使用,以访问超出 64K 限制的内存。对于访问 64K 以上的程序内存,还有特殊的寄存器 RAMPX、RAMPY 和 RAMPZ 可用。

直接数据 LDS/STS

LDS 指令将单个字节的数据从数据空间加载到寄存器中,根据它使用 8 位还是 16 位地址,操作码大小分别为 16 位或 32 位。类似指令可用于使用 STS 指令将单个字节的数据从寄存器传输到数据空间。

Instructions that use this format are; 
                
LDS - Load Direct from Data Space
Syntax:     LDS Rd,k        0«d«31, 0«k«65535
Example:    LDS R0,0x0100

LDS (16 bit) - Load Direct from Data Space
Syntax:     LDS Rd,k        16«d«31, 0«k«127
Example:    LDS R16,0x00

STS - Store Direct to Data Space
Syntax:     STS k,Rr        0«r«31, 0«k«65535
Example:    STS 0x0100,R2

STS (16 bit) - Store Direct to Data Space
Syntax:     STS k,Rr        16«r«31, 0«k«127
Example:    STS 0x00,R2
            

带位移的间接数据 LDD/STD

加载或存储一个字节的数据到数据内存或从数据内存到寄存器。如下图所示,一个立即值被添加到 Y 或 Z 寄存器中的值,以得出所需数据字节的最终地址。在具有超过 64K 数据内存区域的设备上,RAMPY 和 RAMPZ 寄存器允许 24 位寻址。

LDD - Load Indirect using Y or Z
Syntax:     LDD Rd,Y+q    ;0«d«31, 0«q«63
Example:    LDD R4,Y+2    ;Load R4 with loc. Y+2
<br />STD - Store indirect using Y or Z 
Syntax: STD Y+q,Rr ;0«d«31, 0«q«63
Example: STD Y+2,R4 ;Store R4 at loc. Y+2

间接数据 LD/ST

此指令类似于带位移的间接数据,只是它不使用位移,而是使用 X、Y 或 Z 寄存器进行间接加载。

LD - Load Indirect using X, Y or Z
Syntax:     LD Rd,X     0«d«31
            LD Rd,Y
            LD Rd,Z

Example:    LDI R26,0x20
            LD R2,X         ;Load R2 with byte at loc. 0x20
            LDI R28,0x40
            LD R3,Y         ;Load R3 with byte at loc. 0x40
            LDI R30,0x60
            LD R4,Z         ;Load R4 with byte at loc. 0x60           
            

带预减量的间接数据

类似于间接数据指令,此指令在访问数据之前递减 X、Y 或 Z 寄存器,并且像间接数据指令一样,它允许使用寄存器。

LD - Load Indirect using X, Y or Z
Syntax:     LD Rd,X     0«d«31
            LD Rd,Y
            LD Rd,Z

Example:    LDI R26,0x20
            LD R2,-X         ;Load R2 with loc. 0x1F
            LDI R28,0x40
            LD R3,-Y         ;Load R3 with loc. 0x3F
            LDI R30,0x60
            LD R4,-Z         ;Load R4 with loc. 0x5F            
            

带后增量的间接数据

类似于间接数据指令,此指令在访问数据后递增 X、Y 或 Z 寄存器,并且像间接数据指令一样,它允许使用寄存器。

 LD - Load Indirect using X, Y or Z
Syntax:     LD Rd,X     0«d«31
            LD Rd,Y
            LD Rd,Z

Example:    LDI R26,0x20
            LD R2,X+         ;Load R2 with loc. 0x20
            LD R2,X          ;Load R2 with loc. 0x21
            LDI R28,0x40
            LD R3,Y+         ;Load R3 with loc. 0x40
            LD R3,Y          ;Load R3 with loc. 0x41
            LDI R30,0x60
            LD R4,Z+         ;Load R4 with loc. 0x60
            LD R4,Z          ;Load R4 with loc. 0x61            
           

EEPROM 内存

虽然程序和数据内存相当直接、易于理解和编程,但 EEPROM 则完全是另一回事。在汇编中,这不是一件简单的事情,最好在“C”中完成,因为“C”提供了处理 EEPROM 读写的代码。

但对于那些打算使用汇编器读写 EEPROM 的勇敢者,我提供了(直接取自数据手册)用于执行最小功能的代码。有关详细信息,请参阅您特定设备的数据手册。

避免使用最低的 EEPROM 地址,在某些情况下,这个最低地址可能会被破坏,您将丢失数据。由于数据是按照您声明变量的顺序写入的,因此在任何其他变量之前声明一个虚假变量即可。

;
;  The EEPROM_Write routine
;
EEPROM_write:
    ;Wait for completion of previous write
    sbic    eecr,eepe
    rjmp    EEPROM_write
    ;Set up the address (r18:r17) to address register
    out     eearh,r18
    out     eearl,r17
    ;Write data (r16) to Data register
    out     eedr,r16
    ;Write logical one to eempe
    sbi     eecr,eempe
    ;Start eeprom write by setting eepe
    sbi     eecr,eepe
    ret
;
;   The EEPROM_Read routine
;
EEPROM_Read:
    ;Wait for the completion of the previous write
    sbic    eecr,eepe
    rjmp    EEPROM_Read
    ;Set up address (r18:r17) in address register
    out     eearh,r18
    out     eearl,r17
    ;Start eeprom read by writing eere
    sbi     eecr,eere
    ;Read data from Data register
    in      r16,eedr
    ret
    
    

输入/输出

对于大多数设备,IO 寄存器空间映射到常规数据内存,偏移量为 0x20,这意味着它可以像任何其他数据内存一样访问,这包括所有外设(如定时器、USART、看门狗定时器等)的寄存器。

当用作通用 I/O 端口时,所有端口都具有读-修改-写功能,并且每个引脚都具有对称的驱动或灌电流能力。此外,单个引脚可以配置为输入或输出,具有可选的上拉电阻器,并具有到 VCC 和 GND 的保护二极管。

提供了两个特殊指令(IN 和 OUT)用于处理 I/O 寄存器。这些指令如何使用的示例可以在 EEPROM 示例代码中查看。

寄存器使用

通常,与“C”代码一起使用的寄存器遵循下表列出的通用准则。当我们开始混合语言时,我们将研究这些寄存器,它们在集成中起着非常重要的作用。

r0 临时寄存器 - 不建议在中断中使用。
r1 零寄存器 - 可用于临时数据,但使用后必须归零。
r18-r27, r30-r31 这些是通用寄存器,与“C”代码结合使用时无需保存。
r2-r17, r28-r29 这些是通用寄存器,但与“C”代码结合使用时需要保存。

根据定义,宏是一组指令,您只需编写一次,就可以根据需要多次使用。宏和子程序之间的主要区别在于,宏在使用它的地方进行扩展。宏可以接受最多 10 个参数,称为 @0-@9,并以逗号分隔列表形式给出。

;PUSH_REGS macro
;Example macro that accepts 2 parameters that define the 
;registers that are to be pushed onto the stack.
.macro PUSH_REGS
    push    @0
    push    @1
.endmacro
;
;Then to use the PUSH_REGS macro
label:
    ldi         R18,0x00
    ldi         R17,0x02
    PUSH_REGS   R18,R17
    .
    .
;
;And in reality what you end up with is
label:
    ldi         R18,0x00
    ldi         R17,0x02
    push        R18     ;macro code
    push        R17     ;macro code
    .
    .
;
    

宏通常由例行执行的代码组成,并保存在库中,以便在需要时和需要的地方包含它们。

混合语言

GCC 'C' 编译器以非常一致的方式使用寄存器将参数传递给子程序并从子程序返回值。如果我们在混合使用“C”和汇编器等语言时遵守一些简单的规则,那么两种语言的集成就相当简单。本教程中只引用了“C”,但我认为许多使用 GCC 编译器的高级语言也可以以类似的方式引用。

向子程序传递参数时,依次使用寄存器 r25 到 r8。如果需要向子程序传递的参数多于寄存器,则使用堆栈,由于对资源的大量消耗,不建议这样做。另外需要注意的是,无论传递的参数大小如何,都使用寄存器对。这个概念和其他概念将在接下来的两节中进一步讨论。从子程序返回的值遵循下表所示的指导方针。

R24 8 位值
R24-R25 16 位值
R24-R22 32 位值
R24-R18 64 位值

从 C 调用汇编子程序

现在您应该对预期结果有了一个很好的了解,因此我将通过提供几个示例来演示从“C”调用汇编子程序。每个示例都将包含“C”代码,接着是生成的反汇编代码,最后是汇编子程序。

在第一个示例中,汇编子程序将作为参数传递的两个 16 位数字 iParam1 (R25:R24) 和 iParam2 (R23:R22) 相加,并将结果 (R25:R24) 返回给主“C”例程。

//Assembly subroutine declaration - keeps the compiler from
//generating a warning concerning implicit declaration.
int AsmSubroutine(int iParam1, int iParam2);

//This is the main 'C' routine
int main()
{
    int iRetVal = 0;
    
    //Call to our assembler subroutine
    iRetVal = AsmSubroutine(1024, 16);
}
    

生成的反汇编代码

            iRetVal = AsmSubroutine(1024, 16);
 318:    80 e0           ldi    r24, 0x00    ; 0
 31a:    94 e0           ldi    r25, 0x04    ; 4
 31c:    60 e1           ldi    r22, 0x10    ; 16
 31e:    70 e0           ldi    r23, 0x00    ; 0
 320:    0e 94 ae 01     call    0x35c    ; 0x35c <AsmSubroutine>
 324:    90 93 01 06     sts    0x0601, r25
 328:    80 93 00 06     sts    0x0600, r24
        

汇编子程序代码

    .section    .text
    
    ;The global directive declares AsmSubroutine as global for linker.
    ;The AsmSubroutine label must follow the global directive.
    .global     AsmSubroutine
AsmSubroutine:
    add        R25,R23    
    adc        R24,R22
    ret
    .end
    

在第二个示例中,汇编子程序将作为参数传递的两个 8 位数字 iParam1 (R24) 和 iParam2 (R22) 相加,并将结果 (R24) 返回给主“C”例程。

//Assembly subroutine declaration - keeps the compiler from
//generating a warning concerning implicit declaration.
unsigned char AsmSubroutine(unsigned char, unsigned char);

//This is the main 'C' routine
int main()
{
    unsigned char ucRetVal = 0;
    
    //Call to our assembler subroutine
    ucRetVal = AsmSubroutine(32, 16);
}
    

生成的反汇编代码

    iRetVal = AsmSubroutine(32, 16);
 318:    80 e2           ldi    r24, 0x20    ; 32
 31a:    60 e1           ldi    r22, 0x10    ; 16
 31c:    0e 94 aa 01     call    0x354    ; 0x354 <AsmSubroutine>
 320:    80 93 00 06     sts    0x0600, r24
        

汇编子程序代码

    .section    .text
    
    ;The global directive declares AsmSubroutine as global for linker.
    ;The AsmSubroutine label must follow the global directive.
    .global     AsmSubroutine
AsmSubroutine:
    add        R24,R22    
    ret
    .end
    

从这两个示例中可以看出,传递的参数每个参数使用一对寄存器,因此在第二个示例中,即使我们传递了两个 8 位值,编译器也将每个 8 位值放在寄存器对的低位。

从汇编器调用 C 子程序

当从汇编器调用“C”子程序时,相同的规则和寄存器适用,将正确的参数加载到 R25-R18 中,并期望结果出现在相应的寄存器中。为了说明这个概念,我们将像上面第一个示例中那样添加两个 16 位数字,但在从 C 调用汇编子程序后,我们将调用一个 C 例程,该例程将添加这两个数字并返回结果,您将看到将获得相同的结果。

int AsmSubroutine(int, int);
int AddCSubroutine(int, int);

int main()
{
    int iRetVal = 0;
    
    iRetVal = AsmSubroutine(1024, 16);
}

//Adds to 16 bit numbers.
int AddCSubroutine(int p1, int p2)
{
    return p1 + p2;
}
    

如果您将其与上面的第一个示例进行比较,您会注意到它们是相同的。

        iRetVal = AsmSubroutine(1024, 16);
 320:    80 e0           ldi    r24, 0x00    ; 0
 322:    94 e0           ldi    r25, 0x04    ; 4
 324:    60 e1           ldi    r22, 0x10    ; 16
 326:    70 e0           ldi    r23, 0x00    ; 0
 328:    0e 94 b2 01     call    0x364    ; 0x364 <AsmSubroutine>
 32c:    90 93 01 06     sts    0x0601, r25
 330:    80 93 00 06     sts    0x0600, r24
    

汇编子程序只是调用了“C”子程序,这表明在整个过程中使用了相同的寄存器。

    .section    .text
    .global     AsmSubroutine
AsmSubroutine:
    call    AddCSubroutine
    ret
    .end
    

完整的汇编程序示例

这个简单但完整的汇编程序演示了汇编应用程序所需的基本组件。该应用程序从程序内存中读取数据并以相反的顺序将其写入数据内存,演示了如何访问程序和数据内存。该示例注释良好,因此不再提供进一步的解释。

/*
    AVR Assembler Tutorial Example

    Author: Mike Hankey
    Date:    7/17/2010
    Hardware:     ATMega1280
    Assembler:     AVR Assembler 2.0

    Purpose: Read msg data from program memory and write
    it to Data segment in reverse order.

    Although this is a very simple example of an assmbler
    program it contains many of the elements that are
    needed in most real assembler applications.  
*/

.NOLIST
.include "m1280def.inc"
.LIST

/*
    Macro to set the Stack Pointer to end of ram
    Input Parameters: none
*/
.macro SET_STACK
    ldi    r16, LOW(RAMEND)
    out    spl, r16
    ldi    r16, HIGH(RAMEND)
    out    sph, r16
.endmacro

/*
    Data segment
    All we can do here is reserve a    portion of the data 
    segment for our target string.  We cannot initialize
    data in this segment. We are setting aside 32 bytes
    (0x20) for our target string.
*/
.dseg
msgd:
    .byte    0x20

/*
    Code Segment
    Use .org to set the base address in code segment where
    we want the code to begin, in this case 0 or beginning
    of the segment.  The first part of the code segment is
    reserved for the interrupt/jump table and the first 
    item in the table is the reset vector which we put a 
    jump instruction to the first line of our code..  Since
    we are not declaring any other interrupts we can ignore
    the    rest of the table and just add a 2nd .org setting 
    the start label or beginning of the    code at location 
    0x20.
*/
.cseg
.org 0
    rjmp    start

/*
    End of Jump table and start of our code.
*/
.org 0x20
start:
    SET_STACK            ;Invoke our macro to set stack ptr
    /*
        The Z-resister is used to access Program memory.  
        This 16 bit register pair is used as a 16 bit ptr 
        to the Program Memory where the most significant 15 
        bits select the word address and the LSB is the 
        Low/High bit select therefore we must multiply the 
        address by 2 by shifting left one place.  We could 
        have also have just multiplied by 2;
            ldi    ZH,high(msg*2)
            ldi    ZL,low(msg*2)
        either way is acceptible!
    */
     ldi        ZH,high(msg<<1)    ;Set Z pointer to message
    ldi        ZL,low(msg<<1)    
    rcall    get_length        ;call subroutine to get length
    ldi        XH,high(msgd)    ;Set X pointer to destination in
    ldi        XL,low(msgd)    ; data memory.
    add        XL,r17            ;Add count to X pointer,
    /*
        Once we have the length we use it to loop through 
        each item loading it into R24, do a post increment 
        and write the character to the current location 
        pointed to by the X-register pain.  We then 
        decrement the X pointer then the counter and if 
        not zero branch to loop and repeat.
    */
loop:
    lpm        r24,Z+
    st        X,r24
    dec        XL
    dec        r17
    brge    loop
    ret
/*
    Subroutine to count the length of the string that is 
    pointed    to by the Z-register.
        ZH:ZL    Pointer to string
        R17        Calcualted string length
    Upon entry we push the initial value of the Z-register 
    for use later.
    
    We get the length by loading the byte currently pointed 
    to by the Z-register and doing a post increment.  
    We check the byte and if it is not the terminating zero 
    we increment the count and jump to loop to repeat the 
    sequence of instructions.

    Upon exit we restore the intial Z-register values.
*/
get_length:
    push    ZH
    push    ZL
    ldi        r17,0
loop1:
    lpm        r24,Z+
    cpi        r24,0
    breq    exit
    inc        r17
    rjmp        loop1
exit:
    pop        ZL
    pop        ZH
    ret
/*
    Our meesage string..
*/
msg:
    .db        "String to be reversed",0
       
    

结论

在本文中,我试图触及 AVR 汇编语言的重要方面,但它是一个如此广泛的主题,以至于不可能一次性涵盖整个主题。

学习汇编器的最佳方法是浏览代码并查看其他人做了什么,或者用 C 编写一段代码并进入列表文件并查看汇编器列表。但最重要的是,您必须亲自动手。

参考文献

© . All rights reserved.