基于ATMega16的流水灯实例(汇编)

发布时间 2023-12-04 21:27:47作者: fxzq

本例在ATMega16上,利用汇编程序实现一个流水灯,主要讨论寄存器移位及软件延时的使用方法。

本例中的八个LED电路通过限流电阻及跳线帽接在PA端口,电路如下图所示。

完整的汇编代码如下。 

.INCLUDE "M16DEF.INC"
.DEF    TMP = R16                 ;定义一个R16寄存器的别名(R不能小于16)
.DEF    SHIFT = R17               ;定义一个R17寄存器的别名(R不能小于16)
.ORG    $0000
    JMP    RESET
.ORG    $002A
RESET:
    LDI     TMP, HIGH(RAMEND)    ;获取RAM空间的最大地址高字节(ATMega16为$04)
    OUT     SPH, TMP             ;高字节送SP高位
    LDI     TMP, LOW(RAMEND)     ;获取RAM空间的最大地址低字节(ATMega16为$5F)
    OUT     SPL, TMP             ;低字节送SP低位
    SER     TMP                  ;把R16全部置1
    OUT     DDRA, TMP            ;把端口A设置为输出方向
    OUT     PORTA, TMP           ;把端口A输出高电平
    LDI     TMP, 114             ;设置延时的值,约100ms
    LDI     SHIFT, $FE           ;加载值$FE到R17中
    SEC                          ;置进位位C为1
LOOP:
    OUT     PORTA, SHIFT         ;输出R17的值到端口A
    ROL     SHIFT                ;对R17的值进行带进位位的左移
    RCALL   DELAY                ;调用延时子程序
    RJMP    LOOP                 ;跳转到LOOP

DELAY:                           ;延时子程序入口
    PUSH    TMP                  ;R16的值压入栈中
D1:
    PUSH    TMP
D2:
    PUSH    TMP                   ;连压三个R16
D3:
    DEC     TMP                   ;R16的值减一
    BRNE    D3                    ;若R16的值不为0则跳转到del3处
    POP     TMP                   ;R16的值出栈
    DEC     TMP
    BRNE    D2
    POP     TMP
    DEC     TMP
    BRNE    D1
    POP     TMP
    RET                           ;子程序返回

本例使用到了AVR中I/O空间的4个寄存器,即SPH、SPL、DDRA和PORTA。

先看SPH和SPL两个寄存器,它们同属于栈指针寄存器,分别管理高低两个8位,具体如下表所示。

栈指针寄存器一共有16位,可寻址64KB的SRAM空间。初始值为全0,即默认SP指向SRAM的最低地址。由于AVR的栈指针是向下生长型,所以一般在初始化时,要将SP指向SRAM空间的最高地址。在ATMega16中,$0000~$001F是32个通用寄存器空间,$0020~$005F是64个I/O空间寄存器,$0060~$045F才是SRAM空间,共有1KB,因此在初始化时,SP指向$045F地址。

再来看DDRA寄存器,它称为端口方向寄存器,用于设置端口是输出方向还是输入方向,具体如下表所示。

当某位被设置为1时,该位对应的引脚被设置为输出方向,设置为0时,为输入方向。复位后的初始值为0,即默认为输入方向。在ATMega16中,这样的方向寄存器一共有4个,即DDRA、DDRB、DDRC和DDRD,配置方法完全一样。

接着看PORTA寄存器,它称为端口数据寄存器,用于设置引脚电平,具体如下表所示。

当某位被设置为1时,该位对应的引脚输出高电平,设置为0时,输出低电平。复位后的初始值为0,即默认引脚输出低电平。在ATMega16中,这样的方向寄存器一共有4个,即PORTA、PORTB、PORTC和PORTD,配置方法完全一样。

本例中一共使用到了13条指令,具体解释如下。

1)直接跳转
  JMP    k    0 ≤ k < 4M
说明:直接跳转到指定的地址处(可达4M字的程序存储器),该指令不一定支持所有的AVR芯片。
操作:PC ← k   32位机器码:1001 010k kkkk 110k kkkk kkkk kkkk kkkk

2)立即数送寄存器
  LDI   Rd, K    16 ≤ d ≤ 31,0 ≤ K ≤ 255
说明:装入一个8位的立即数到寄存器R16~R31中。
操作:Rd ← K   PC ← PC + 1   16位机器码:1110 KKKK dddd KKKK 

3)寄存器数据送I/O空间
  OUT   A, Rr    0 ≤ r ≤ 31,    0 ≤ A ≤ 63
说明:将寄存器Rr中的数据传送到I/O空间。
操作:I/O(A) ← Rr    PC ← PC + 1   16位机器码:1011 1AAr rrrr AAAA

4)置寄存器为全1
  SER    Rd    16 ≤ d ≤ 31
说明:将$FF直接装入目的寄存器Rd。
操作:Rd ← $FF   PC ← PC + 1   16位机器码:1110 1111 dddd 1111

5)进位位置1
  SEC
说明:将进位标志位(C)置1。
操作:C ← 1   PC ← PC + 1   16位机器码:1001 0100 0000 1000

6)带进位位的寄存器循环左移
  ROL    Rd    0 ≤ d ≤ 31
说明:寄存器Rd中所有位左移1位,C标志被移到Rd的第0位,Rd的第7位移到C标志。
操作:C ← b7--------b0 ← C    PC ← PC + 1   16位机器码:0001 11dd dddd dddd

7)相对跳转
  RJMP    k    -2k ≤ k < 2k
说明:相对跳转到PC - 2K +1 ~ PC + 2K (字) 范围内的地址,对于程序存储器空间不超过4K字(8K 字节)的芯片,该指令可以寻址整个程序存储器空间的每一个地址。
操作:PC ← PC + k + 1   16位机器码:1100 kkkk kkkk kkkk

8)相对调用
  RCALL    k    -2k ≤ k < 2k
说明:相对调用PC-2K+1到PC+2K (字)范围内的子程序,返回地址(RCALL后面那条指令的地址)保存到堆栈当中。对于程序存储器不超过4K字(8K字节)的AVR芯片,这条指令可以寻址整个程序存储器空间。在调用RCALL指令时,堆栈指针是带后减量的(调用完成后减少)。
操作:STACK ← PC + 1   SP ← SP - 2 (2 字节, 16 位)    PC ← PC + 1 + k    16位机器码:1101 kkkk kkkk kkkk

9)压栈
  PUSH    Rr    0 ≤ r ≤ 31
说明:存储寄存器Rr中的数据到堆栈,堆栈的指针在PUSH后加1。该指令不是对所有的AVR芯片都有效。
操作:STACK ← Rr    SP ← SP - 1   PC ← PC + 1   16位机器码:1001001ddddd1111

10)出栈
  POP    Rd    0 ≤ d ≤ 31
说明:将堆栈中的字节装入寄存器Rd中,堆栈指针在POP之前首先减1。该指令不是对所有的AVR芯片都有效。
操作:Rd ← STACK    SP ← SP + 1   PC ← PC + 1   16位机器码:1001 000d dddd 1111

11)减1
  DEC   Rd    0 ≤ d ≤ 31
说明:寄存器Rd内容减1,并将结果置于目标寄存器Rd中。
操作:Rd ← Rd - 1    PC ← PC + 1   16位机器码:1001 010d dddd 1010

12)不相等跳转
  BRNE    k    -64 ≤ k ≤ +63
说明:条件相对跳转,测试零标志位Z,如果Z位被清零,则相对PC值跳转k个字。如果在执行CP、CPI、SUB或 SUBI指令后,立即执行该指令,且当寄存器Rd中数与寄存器Rr中数不相等时,将发生跳转。可跳转k个字,k 为7位带符号数,最多可向前跳63个字,向后跳64个字。
操作:If Rd ≠ Rr (Z = 0) then PC ← PC + k + 1, else PC ← PC + 1   16位机器码:1111 01kk kkkk k001

13)子程序返回
  RET
说明:从子程序返回,返回地址从堆栈中获得。在执行RET时堆栈指针带有预增量(调用前堆栈指针增加)。
操作:SP←SP + 2   PC(15:0) ← STACK    16位机器码:1001 0101 0000 1000

此外,程序中还用到了一些伪指令,具体解释如下。

1)包含指定的文件
语法:.INCLUDE“文件名”
说明:通知汇编器开始从一个指定的文件中读入程序语句,并对读入的语句进行编译,直到该包含文件结束或遇到该文件中的EXIT伪指令,然后再从本文件当前INCLUDE伪指令的下一行语句处继续编译。在一个包含文件中,也可以使用INCLUDE伪指令来包含另外一个指定的文件。

2)定义寄存器符号名
语法:.DEF 符号名 = 寄存器
说明:给寄存器定义一个替代的符号名。在后续程序中可以使用定义的符号名来表示被定义的寄存器。可以给一个寄存器定义多个符号名。符号名在后续程序中可以重新指定。编译时,凡遇到符号名,都以相应被定义的寄存器替代。

3)定义代码起始位置
语法:.ORG 表达式
说明:设置定位计数器为一个绝对数值,该数值为表达式的值,作为代码的起始位置。如果该伪指令出现在数据段中,则设定SRAM定位计数器;如果该伪指令出现在代码段中,则设定程序存储器计数器;如果该伪指令出现在EEPROM段中,则设定EEPROM定位计数器。如果该伪指令前带标号(在相同的语句行),则标号的定位由ORG的参数值定义。代码段和EEPROM段定位计数器的默认值是0;而当汇编器启动时,SRAM定位计数器的默认值是32(因为寄存器占有地址为0~31)。文件中可以出现多处ORG伪指令,但后面ORG所带的地址不能小于前一个ORG所带的地址,也不能落在由前一个ORG定位的代码段空间(指同一个存储器空间)。注意:EEPROM和SRAM定位计数器按字节计数,而程序存储器定位计数器按字计数。

下面对程序中的相关部分进行一下说明。

1)在AVR Studio的汇编语言环境中,十六进制数以$符号开头。

2)主程序的入口地址为$002A,因为在ATMega16中,从$0000~$002A之间有21个中断源的向量地址,主程序应该尽量避开这些地址,以免与中断程序冲突。

3)在AVR中,要访问I/O空间的寄存器,必须依靠某个通用寄存器来进行,不允许直接对I/O寄存器进行赋值。比如例程中,对SP寄存器(以及后续的DDRA、PORTA寄存器)的赋值只能通过通用寄存器TMP(R16)来中转。 

4)由于程序中使用了LDI和SER指令,所以通用寄存器TMP只能取R16~R31之间的值。

5)由于8个LED为共阳结构,所以流水灯采用0的循环左移即可。本例采用带进位位的左移,这样可以保证循环中始终有一个0,免去了左移到头的判断。

6)延时程序采取了空耗机器周期的方式,其中仅使用了一个寄存器(TMP),采用二次嵌套循环,并多次利用堆栈交换数据。它能在8MHz晶振时产生出长达1秒的延时,总机器周期数T与寄存器值x之间有如下关系。

总延时时间(秒)= T * 机器周期。如果晶振为8MHz,则机器机器周期为0.125us。本例中,x=114,T=800404,延时时间为100ms左右。