AVR 汇编入门 101






4.96/5 (35投票s)
学习 AVR 微控制器和汇编语言的基础知识
可供下载的 AVR 汇编器画笔 shBrushAsm.js.zip 是一个脚本,与 Alex Gorbatchev 的 Syntaxhighlighter 脚本结合使用。它尚未经过全面测试,因此如果您发现任何问题,请告诉我。
为了学习汇编语言编程,我们必须了解我们正在使用的硬件。在本教程中,我们将首先简要介绍 AVR 微控制器的工作原理,然后转到纯汇编器,最后展示如何混合使用“C”和汇编语言。
引言
既然有 C、C++ 等提供抽象层的语言可以消除编程的所有繁琐工作,为什么还会有人想用汇编器这样的低级语言进行编程呢?
- 有些人是受虐狂。
- 高级编译器生成的代码无法适应 MPU 的内存空间。
- 我们需要速度。
- 我们是控制狂,需要控制应用程序流程的每个方面。
学习汇编器还有另一个很好的理由,您对处理器内部工作原理了解得越多,您就会成为一个越有能力的程序员。即使您确实决定需要用汇编器编写部分代码,您也不局限于只使用汇编器或高级语言,只要我们遵守一些简单的规则,我们就可以将它们混合使用。例如,我们可以将“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 编写一段代码并进入列表文件并查看汇编器列表。但最重要的是,您必须亲自动手。