基于ATMega16的数码管动态扫描实例(汇编)
本例在ATMega16上,利用汇编程序实现8个七段数码的动态扫描显示字符12345678,主要讨论定时器及其中断的使用方法。
本例中的8位数码管采用两个4位的组合而成,段码端通过限流电阻及跳线帽接在PB端口,位选端通过PNP三极管扩流后接在PA端口,电路如下图所示。
完整的汇编代码如下。
.INCLUDE "M16DEF.INC" .DEF TMP = R16 ;定义R16寄存器的别名(注意小于16号的寄存器不能进行LDI操作) .DEF TMP1 = R17 .DEF CNT = R18 .DEF SHIFT = R19 .ORG $0000 ;NOP ;RJMP RESET JMP RESET ;注意,RJMP是单字指令,JMP是双字指令,下面中断向量入口地址是双字对齐的 NOP ;NOP空指令,是单字指令 RETI ;RETI中断返回,是单字指令 NOP RETI NOP RETI NOP RETI NOP RETI NOP RETI NOP RETI NOP RJMP TIME1_OVF ;定时器1的溢出中断向量入口,地址为$10 NOP RETI NOP RETI NOP RETI NOP RETI NOP RETI NOP RETI NOP RETI NOP RETI NOP RETI NOP RETI NOP RETI NOP RETI ;前面最后一个中断向量入口地址是$28,双字后刚好是$2A 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 DDRB, TMP ;把端口B设置为输出方向 LDI SHIFT, $FE ;从端口A的第0位开始扫描 LDI CNT, 1 ;从1开始显示 LDI ZL, LOW(LED7 * 2) ;获取字形码所在的首地址低字节 LDI ZH, HIGH(LED7 * 2) ;获取字形码所在的首地址高字节,乘2的目的是让整个字地址左移1位,空出最低位来作为高/低字节选择 ;定时器Timer1溢出中断配置 SER TMP LDI TMP1, $83 OUT TCNT1H, TMP OUT TCNT1L, TMP1 ;以上为设置定时器1的初始值 CLR TMP ;置R16为全0 LDI TMP1, $03 OUT TCCR1A, TMP OUT TCCR1B, TMP1 ;设置为64分频,根据上面的初始值,8MHz晶振对应1ms LDI TMP, $04 OUT TIMSK, TMP ;允许定时器1的溢出中断 SEI ;全局中断允许 LOOP: RJMP LOOP ;定时器Timer1溢出中断服务程序 TIME1_OVF: CLI ;关闭全局中断 SER TMP LDI TMP1, 0x83 OUT TCNT1H, TMP OUT TCNT1L, TMP1 ;重载定时器初始值 SEI ;开启全局中断 LDI ZL, LOW(LED7 * 2) ;获取字形码所在的首地址低字节,字形码的数量不超过255,所以可以不用再次获取地址高字节,因为没改动过 ADD ZL, CNT ;低字节加上查表的偏移量(即要显示的值) LPM ;查表后的内容送入R0中 OUT PORTB, R0 ;把R0的内容送显 OUT PORTA, SHIFT ;打开相应的位 SEC ;进位位C置1 ROL SHIFT ;带进位位左移 INC CNT ;显示内容加一 BRCC NEXT ;如查进位位C的值为0,则跳到NEXT执行,否则顺序执行 RETI ;中断返回 NEXT: ;扫描到头 LDI SHIFT, $FE ;从端口A的第0位开始扫描 LDI CNT, 1 ;显示内容复位 RETI ;中断返回 .CSEG ;以下数据放置在Code区域,即Flash中 LED7: ;字形码数据,以字节方式顺序存放 .DB $03,$9F,$25,$0D,$99,$49,$41,$1F .DB $01,$09,$11,$C1,$63,$85,$61,$71
本例使用到了AVR中I/O空间的11个寄存器,即SPH、SPL、DDRA、DDRB、PORTA、PORTB、TCNT1L、TCNT1H、TCCR1A、TCCR1B和TIMSK。其中SPH、SPL、DDRA、DDRB、PORTA、PORTB等6个寄存器的介绍可参考“基于ATMega16的流水灯实例”一文。
先来看TCNT1L、TCNT1H两个寄存器,它们构成了定时器T1的计数寄存器,分别由高低两个8位组成,一共16位,具体如下表所示。
定时器T1的计数位宽为16位,初始值为0,最大计数值为65535。要改变T1的初始计数值,直接写这两个寄存器即可。
再来看TCCR1A和TCCR1B两个寄存器,它们同属于定时器T1的控制寄存器,初始值为全0,具体如下表所示。
T1的工作模式由TCCR1A中的第0、1两位(WGM10 、WGM11)和TCCR1B中的第3、4两位(WGM12 、WGM13)共同决定,这里需要它们为全0,即让T1工作在普通定时模式。此外,由TCCR1B的低3位确定时钟的分频情况,具体如下。
在本例中以上3位设置为011,即选择时钟的64分频。
最后来看TIMSK寄存器,它是定时器的中断屏蔽寄存器,具体如下表所示。
表中的第0、2、6位分别是定时器T0、T1和T2的溢出中断使能控制位,把控制位置1就可以使能相应定时器溢出中断(同时还要使能总中断)。
本例中一共使用到了15种指令,其中的JMP、RJMP、LDI、OUT、SER、SEC、ROL等7条指令可参考“基于ATMega16的流水灯实例”一文。 其余8条指令解释如下。
1)空指令
NOP
说明:这条指令在一个周期内不作任何操作,即只占用一个机器周期。
操作:PC ← PC + 1 16位机器码:0000 0000 0000 0000
2)中断返回
RETI
说明:从中断程序返回,返回的地址从堆栈中获得,并将全局中断标志置位。在本指令中堆栈指针带有预增量。
操作:PC ← PC + 2 16位机器码:1001 0101 0001 1000
3)寄存器清零
CLR Rd 0 ≤ d ≤ 31
说明:寄存器清零,该指令采用寄存器Rd与自己的内容相异或实现寄存器的所有位都被清零。
操作:Rd ← Rd ⊕ Rd PC ← PC + 1 16位机器码:0010 01dd dddd dddd
4)使能全局中断
SEI
说明:将状态寄存器的全局中断标志位置1,应在响应中断之前执行这条指令。
操作:PC ← PC + 1 16位机器码:1001 0100 0111 1000
5)不带进位加法
ADD Rd,Rr 0 ≤ d ≤ 31,0 ≤ r ≤ 31
说明:两个寄存器不带进位C标志位相加,结果送目的寄存器Rd。
操作:Rd ← Rd + Rr PC ← PC + 1 16位机器码:0000 11rd dddd rrrr
6)从程序存储器中取数装入寄存器R0
LPM
说明:从Z寄存器指定的程序空间中装入一个字节的数据到目的寄存器R0。这个指令可以获得100%数据空间中的有效常量初值,或常量数据。程序存储器是按16位的字来组织的,而Z指针指向字节地址,所以Z指针的最低有效位要么选定程序区的低字节(ZLSB=0)要么选定高字节(ZLSB=1),这条指令可以寻址程序存储器的前64K 字节(32K字)空间。在操作中,Z指针寄存器可以不变、带预减量、或者带后增量。不是所有AVR芯片都支持这条指令的各种变形。
操作:R0 ← (Z) PC ← PC + 1 16位机器码:1001 0101 1100 1000
7)增1指令
INC Rd 0 ≤ d ≤ 31
说明:寄存器Rd的内容加1,结果送目的寄存器Rd中。该操作不改变SREG中的C标志,所以INC指令允许在多倍字长计算中用作循环计数。当对无符号数操作时,仅有BREQ(相等跳转)和BRNE(不相等跳转)指令有效。当对2进制补码值进行操作时,所有的带符号跳转指令都有效。
操作:Rd ← Rd + 1 PC ← PC + 1 16位机器码:1001 010d dddd 0011
8)进位标志位C为0跳转
BRCC k -64 ≤ k ≤ 63
说明:条件相对跳转,测试进位标志C,如果C位被清零,则相对PC值跳转k个字。k 为7位带符号数,最多可向前跳63个字,向后跳64个字。这条指令相当于指令“BRBC 0,k”。
操作:If C = 0 then PC ← PC + k + 1, else PC ← PC + 1 16位机器码:1111 01kk kkkk k000
此外,程序中还用到了另外一些伪指令(.INCLUDE、.DEF、.ORG可参考“基于ATMega16的流水灯实例”一文),具体解释如下。
1)声明代码段(Flash)
语法:.CSEG
说明:用于声明代码段的起始(在Flash中),即CSEG之后是要写入程序存储器中的指令代码、常数表格等。一个汇编程序可包含几个代码段,程序中默认的段为代码段,这些代码段在编译过程中被连接成一个代码段。每个代码段内部都有自己的字定位计数器。可使用ORG伪指令定义该字定位计数器的初始值,作为代码段在程序存储器中的起始位置。CSEG伪指令不带参数。在代码段中不能使用BYTE伪指令。
2)在程序存储器或EEPROM存储器中定义字节常数
语法:.DB
说明:从程序存储器或EEPROM存储器的某个地址单元开始,存入一组规定的8位二进制常数(字节常数)。DB伪指令只能出现在代码段或EEPROM段中。DB伪指令前应使用一个标号,以标记所定义的字节常数区域的起始位置。DB伪指令为一个表达式列表,表达式列表由多个表达式组成,但至少要含有一个表达式,表达式之间用逗号分隔。每个表达式值的范围必须在-128~255之间。如果表达式的值是负数,则用8位2的补码表示,存入程序存储器或EEPROM存储器中。如果DB伪指令用在代码段,并且表达式表中多于一个表达式,则以2字节组合成1字放在程序存储器中。如果表达式的个数是奇数,那么不管下一行汇编代码是否仍是DB伪指令,最后一个表达式的值将单独以字的格式放在程序存储器中。
下面对程序中的相关部分进行一下说明。
1)由于在ATMega16中,从$0000~$002A之间有21个中断源的向量地址,因此程序在开头部分,对齐每一个中断向量入口地址都加入了相关的指令。除了复位和T1溢出两个中断外,其余没有使用到的中断都在各自的向量入口处加入了中断返回指令。这样做能加强系统的健壮性,当未使能的中断因某些原因引发中断时,都能够及时返回,而不会引发意外。
2)由于相邻两个中断向量的地址间隔为两个字,而中断返回指令RETI是单字指令,为了对齐入口地址,需要在每个RETI指令前加入一个空指令NOP,它也为单字指令。
3)定时器T1工作在普通定时模式时,其初始化配置为:首先通过设置TCCR1B寄存器来分频计数时钟,然后通过写TCNT1寄存器来确定初始计数值,接着通过设置TIMSK寄存器来允许溢出中断,最后通过SEI指令打开全局中断。
4)本例中的系统时钟为8MHz,时钟经过64分频后注入TCNT1,TCNT1在初始值$FF83的基础上计数,来一个时钟增加一次,直到溢出。以本例的时钟分频值及定时器的初始值,在T1溢出时刚好定时1ms,溢出后程序指针PC自动跳到T1的溢出中断向量地址处($0010),通过执行跳转指令RJMP跳转到标号TIME1_OVF处去执行中断服务程序。在中断服务程序中,先要进行TCNT1的初始值重载,因为溢出后TCNT1默认是从$0000开始计数的。定时器T1在计数时不需要CPU的参与,所以CPU在定时时间1ms未到时可以执行其他任务,直到T1溢出向CPU发出中断申请时,CPU才去响应并执行中断服务程序。
5)在对T1的双字节(单字)寄存器(比如TCNT1)进行操作时,为了保持同步,高低两个8位要一起操作。在同步读取时,应先读取寄存器的低8位,再立即读取高8位。在同步写入时,应先写入寄存器的高8位,再立即写入低8位。此外,在对这些寄存器进行操作前,最好将中断响应屏蔽掉,以防止读写失误发生。
6)本例采用查表的方式来显示字符,其原理如下。首先通过伪指令.DB把字形编码按字节的方式顺序(顺序为字形0~字形F)存放在Flash空间中,其存储的起始地址为标号LED7。这样一来,地址LED7处存放的为字形0的编码$03(1字节),紧接着存放字形1的编码$9F(1字节),以此类推。由于在ATMega16中Flash的地址是按字(双字节)来编的,所以一个地址可以存放两个字形编码。
7)在查表时,为了把两个字形编码取出来,需要在一个地址空间中把存储的高低两个字节区分出来,为此使用了LPM指令。LPM指令把寄存器Z(由R30和R31合并而成)中存放的数据作为地址,按该地址在Flash中进行查找,并把找到的内容送入寄存器R0。LPM指令规定,寄存器Z中的高15位为Flash中的字地址,最低位为0时指定该字地址的低字节部分,为1时指定高字节部分。可以认为此时寄存器Z中的地址为“字节地址”,这样就可以把Flash中同一个地址的高低两个字节区分出来了。但是,在把Flash中的字地址送给寄存器Z之前,需要先把字地址进行左移1位的操作(即进行乘2的操作),以便让出最低位。由此可见,因为寄存器Z为16位,所以一共可以寻址64K的字节空间,但字地址只有15位(左移了1位),因此一共可以寻址32K的字空间(ATMega16的PC位宽为13,只能寻址8K的字空间)。
8)上述程序中通过执行“LDI ZL, LOW(LED7 * 2)及”LDI ZH, HIGH(LED7 * 2)“两句,就把存储字形编码的首地址(LED7)左移1位后放入了寄存器Z中,这里的LOW为取低8位,HIGH为取高8位,ZL为寄存器Z的低8位(即R30),ZH为高8位(即R31)。此时的寄存器Z中的值可认为是”字节地址“,因此只需要指定偏移量就可在顺序中找到需要的字形编码了。比如偏移量为0,则表示首地址LED7处偏移0个字节,即第0个值$03(字形0),若偏移量为6,则表示首地址LED7处偏移6个字节,即第6个值$41(字形6),这样就把偏移量和显示的字形对应起来了。
9)有了以上的对应关系,查表就容易了。只需要把欲显示的数(存放在CNT中)与寄存器Z的值相加(程序中的ADD ZL, CNT一句),然后再调用LPM指令就可把查表得到的字形数据放入寄存器R0中,接下来只需要把R0中的数据送去段码端进行显示就行了(程序中的OUT PORTB, R0一句)。
10)数码管的扫描利用连续左移来实现。存放在寄存器SHIFT(即R18)中初始值为$FE(即二进制数11111110),从第0位开始扫描(左移),但在执行左移指令时,最右边一位填充的是0而不是1,所以为了保证只有一个0,必须让左移后最右边一位填充1。本例使用的方法是,先把进位位置1,然后执行带进位位的左移ROL指令,这样就保证了左移后SHIFT中的值为11111101。
11)当左移了8次后,所以数码管都被点亮了一次,此时需要重复扫描,即把SHIFT中的值恢复为$FE。本例中为了简化操作,实行了左移9次,即当进位位C中的值为0时,把SHIFT中的值恢复为$FE。判定进位位C中的值是否为0,使用了指令BRCC,即C为0时跳转执行。