基于ATMega16的流水灯实例(汇编)
本例在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左右。