本例在ATMega16上,利用汇编程序通过8个七段数码实现具有时分秒的实时时钟显示,主要讨论定时器T2中异步时钟的使用方法及时钟信号的产生。
本例中的8位数码管采用两个4位的组合而成,段码端通过限流电阻及跳线帽接在PB端口,位选端通过PNP三极管扩流后接在PA端口,电路如下图所示。
完整的汇编代码如下。
.INCLUDE "M16DEF.INC" .DEF TMP = R16 ;定义R16寄存器的别名(注意小于16号的寄存器不能进行LDI操作) .DEF CNT = R17 .DEF SHIFT = R18 .DSEG ;以下数据放置在Data区域,即RAM中 .ORG $0060 ;从$60地址开始存放,即RAM的起始地址 .EQU SECOND = $60 ;定义一个存放秒的RAM空间别名 .EQU MINUTE = $61 ;定义一个存放分的RAM空间别名 .EQU HOUR = $62 ;定义一个存放时的RAM空间别名 .CSEG ;以下数据放置在Code区域,即Flash中 .ORG $0000 JMP RESET ;复位的向量入口,地址为$00 NOP ;空指令 RETI ;中断返回 NOP RETI NOP RETI NOP RJMP TIME2_OVF ;定时器2的溢出中断向量入口,地址为$10 NOP RETI NOP RETI NOP RETI NOP RETI NOP RJMP TIME0_OVF ;定时器0的溢出中断向量入口,地址为$12 NOP RETI NOP RETI NOP RETI NOP RETI NOP RETI NOP RETI NOP RETI NOP RETI NOP RETI NOP RETI NOP RETI .ORG $002A ;前面最后一个中断向量入口地址是$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 ZL, LOW(LED7 * 2) ;获取字形码所在的首地址低字节 LDI ZH, HIGH(LED7 * 2) ;获取字形码所在的首地址高字节,乘2的目的是让整个字地址左移1位,空出最低位来作为高/低字节选择 CLR TMP STS SECOND, TMP ;清0秒的值 LDI TMP, $59 STS MINUTE, TMP LDI TMP, $23 STS HOUR, TMP ;Timer0溢出中断配置 LDI TMP, $06 OUT TCNT0, TMP ;以上为设置定时器0的初始值 IN TMP, TCCR0 ORI TMP, $03 OUT TCCR0, TMP ;设置为64分频,根据上面的初始值,8MHz晶振对应2ms IN TMP, TIMSK ORI TMP, $01 OUT TIMSK, TMP ;允许定时器0的溢出中断 ;Timer2溢出中断配置(异步方式) LDI TMP, $05 OUT TCCR2, TMP ;设置为128分频 CLR TMP OUT TCNT2, TMP ;计数值置0 LDI TMP, $08 OUT ASSR, TMP ;设置定时器2为异步时钟模式 WT: IN TMP, ASSR CPI TMP, $08 BRNE WT ;等待ASSR寄存器的低三位为0 IN TMP, TIFR ORI TMP, $40 OUT TIFR, TMP ;强制清除定时器2的中断标志位 IN TMP, TIMSK ORI TMP, $40 OUT TIMSK, TMP ;允许定时器2的溢出中断 SEI ;开启总中断 SEC ;进位位置1 LOOP: RJMP LOOP ;定时器2溢出中断服务程序(时钟) TIME2_OVF: LDS TMP, SECOND ;获取当前秒的值 INC TMP ;秒加1 CPI TMP, $5A ;秒是否到60 BREQ M ;秒到60则跳到M行执行 CPI TMP, 10 ;秒未60时,如果个位大于10(0x0A),则进行BCD调整 BRHS SOK ;若半进位位H为1,则跳到SOK行执行 SUBI TMP, $FA ;减去$FA,进行BCD调整 SOK: STS SECOND, TMP ;回写秒的值 RETI ;中断返回 M: ;分部分 CLR TMP STS SECOND, TMP ;秒清0 LDS TMP, MINUTE ;获取当前分的值 INC TMP ;分加1 CPI TMP, $5A ;分是否到60 BREQ H ;分到60则跳到H行执行 CPI TMP, 10 ;分未60时,如果个位大于10(0x0A),则进行BCD调整 BRHS MOK ;若半进位位H为1,则跳到MOK行执行 SUBI TMP, $FA ;减去$FA,进行BCD调整 MOK: STS MINUTE, TMP ;回写分的值 RETI ;中断返回 H: ;时部分 CLR TMP STS MINUTE, TMP ;分清0 LDS TMP, HOUR ;获取当前时的值 INC TMP ;时加1 CPI TMP, $24 ;时是否到24 BREQ R ;时到24则跳到R行执行 CPI TMP, 10 ;时未24时,如果个位大于10(0x0A),则进行BCD调整 BRHS HOK ;若半进位位H为1,则跳到HOK行执行 SUBI TMP, $FA ;减去$FA,进行BCD调整 HOK: STS HOUR, TMP ;回写时的值 RETI ;中断返回 R: CLR TMP STS HOUR, TMP ;时清0 RETI ;中断返回 ;定时器Timer0溢出中断服务程序 TIME0_OVF: LDI TMP, $06 OUT TCNT0, TMP ;重载定时器初始值 SBRS SHIFT, 7 ;扫描的第7位为1则继续看第6位 RJMP MG ;扫描的第7位为0则跳到MG(秒的个位)去执行 SBRS SHIFT, 6 ;扫描的第6位为1则继续看第5位 RJMP MS ;扫描的第6位为0则跳到MS(秒的十位)去执行 SBRS SHIFT, 5 ;扫描的第5位为1则继续看第4位 RJMP FGF ;扫描的第5位为0则跳到FGF(分隔符)去执行 SBRS SHIFT, 4 ;扫描的第4位为1则继续看第3位 RJMP FG ;扫描的第4位为0则跳到FG(分的个位)去执行 SBRS SHIFT, 3 ;扫描的第3位为1则继续看第2位 RJMP FS ;扫描的第3位为0则跳到FS(分的十位)去执行 SBRS SHIFT, 2 ;扫描的第2位为1则继续看第1位 RJMP FGF ;扫描的第2位为0则跳到FGF(分隔符)去执行 SBRS SHIFT, 1 ;扫描的第1位为1则继续看第0位 RJMP SG ;扫描的第1位为0则跳到SG(时的个位)去执行 SBRS SHIFT, 0 ;扫描的第0位为1则跳一行执行 RJMP SS ;扫描的第0位为0则跳到SS(时的十位)去执行 SEC ;进位位C置1,由于下面用到带进位位的左移 ROL SHIFT ;带进位位左移 RETI ;中断返回 SS: LDI ZL, LOW(LED7 * 2) ;获取字形码所在的首地址低字节,字形码的数量不超过255,所以可以不用再次获取地址高字节,因为没改动过 LDS CNT, HOUR ;获取时的当前值 ANDI CNT, $F0 ;屏蔽低4位 SWAP CNT ;高低4位交换 ADD ZL, CNT ;低字节加上要显示的时十位值 LPM ;查表后的内容送入R0中 OUT PORTB, R0 ;把R0的内容送显 OUT PORTA, SHIFT ;打开显示的第7位 SEC ;进位位C置1,由于下面用到带进位位的左移 ROL SHIFT ;带进位位左移 BRCC BRIDGE ;如查进位位C的值为0,则跳到NEXT执行(此处超出了跳转范围,故借用RJMP来实现),否则顺序执行 RETI ;如果还没扫描到头,则中断返回 BRIDGE: ;桥接跳转 RJMP NEXT SG: LDI ZL, LOW(LED7 * 2) ;获取字形码所在的首地址低字节,字形码的数量不超过255,所以可以不用再次获取地址高字节,因为没改动过 LDS CNT, HOUR ;获取时的当前值 ANDI CNT, $0F ;屏蔽高4位 ADD ZL, CNT ;低字节加上要显示的时十位值 LPM ;查表后的内容送入R0中 OUT PORTB, R0 ;把R0的内容送显 OUT PORTA, SHIFT ;打开显示的第6位 SEC ;进位位C置1,由于下面用到带进位位的左移 ROL SHIFT ;带进位位左移 BRCC NEXT ;如查进位位C的值为0,则跳到NEXT执行,否则顺序执行 RETI ;如果还没扫描到头,则中断返回 FS: LDI ZL, LOW(LED7 * 2) ;获取字形码所在的首地址低字节,字形码的数量不超过255,所以可以不用再次获取地址高字节,因为没改动过 LDS CNT, MINUTE ;获取时的当前值 ANDI CNT, $F0 ;屏蔽低4位 SWAP CNT ;高低4位交换 ADD ZL, CNT ;低字节加上要显示的时十位值 LPM ;查表后的内容送入R0中 OUT PORTB, R0 ;把R0的内容送显 OUT PORTA, SHIFT ;打开显示的第4位 SEC ;进位位C置1,由于下面用到带进位位的左移 ROL SHIFT ;带进位位左移 BRCC NEXT ;如查进位位C的值为0,则跳到NEXT执行,否则顺序执行 RETI ;如果还没扫描到头,则中断返回 FG: LDI ZL, LOW(LED7 * 2) ;获取字形码所在的首地址低字节,字形码的数量不超过255,所以可以不用再次获取地址高字节,因为没改动过 LDS CNT, MINUTE ;获取时的当前值 ANDI CNT, $0F ;屏蔽高4位 ADD ZL, CNT ;低字节加上要显示的时十位值 LPM ;查表后的内容送入R0中 OUT PORTB, R0 ;把R0的内容送显 OUT PORTA, SHIFT ;打开显示的第3位 SEC ;进位位C置1,由于下面用到带进位位的左移 ROL SHIFT ;带进位位左移 BRCC NEXT ;如查进位位C的值为0,则跳到NEXT执行,否则顺序执行 RETI ;如果还没扫描到头,则中断返回 MS: LDI ZL, LOW(LED7 * 2) ;获取字形码所在的首地址低字节,字形码的数量不超过255,所以可以不用再次获取地址高字节,因为没改动过 LDS CNT, SECOND ;获取时的当前值 ANDI CNT, $F0 ;屏蔽低4位 SWAP CNT ;高低4位交换 ADD ZL, CNT ;低字节加上要显示的时十位值 LPM ;查表后的内容送入R0中 OUT PORTB, R0 ;把R0的内容送显 OUT PORTA, SHIFT ;打开显示的第1位 SEC ;进位位C置1,由于下面用到带进位位的左移 ROL SHIFT ;带进位位左移 BRCC NEXT ;如查进位位C的值为0,则跳到NEXT执行,否则顺序执行 RETI ;如果还没扫描到头,则中断返回 MG: LDI ZL, LOW(LED7 * 2) ;获取字形码所在的首地址低字节,字形码的数量不超过255,所以可以不用再次获取地址高字节,因为没改动过 LDS CNT, SECOND ;获取时的当前值 ANDI CNT, $0F ;屏蔽高4位 ADD ZL, CNT ;低字节加上要显示的时十位值 LPM ;查表后的内容送入R0中 OUT PORTB, R0 ;把R0的内容送显 OUT PORTA, SHIFT ;打开显示的第0位 SEC ;进位位C置1,由于下面用到带进位位的左移 ROL SHIFT ;带进位位左移 BRCC NEXT ;如查进位位C的值为0,则跳到NEXT执行,否则顺序执行 RETI ;如果还没扫描到头,则中断返回 FGF: LDI TMP, $FD OUT PORTB, TMP ;把字符-送显 OUT PORTA, SHIFT ;打开显示的第6位和第1位 SEC ;进位位C置1,由于下面用到带进位位的左移 ROL SHIFT ;带进位位左移 BRCC NEXT ;如查进位位C的值为0,则跳到NEXT执行,否则顺序执行 RETI NEXT: ;扫描到头 LDI SHIFT, $FE ;从端口A的第0位开始扫描 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空间的13个寄存器,即SPH、SPL、DDRA、DDRB、PORTA、PORTB、TCNT0、TCNT2、TCCR0、TCCR2、ASSR、TIFR和TIMSK。其中SPH、SPL、DDRA、DDRB、PORTA、PORTB等6个寄存器的介绍可参考“基于ATMega16的流水灯实例”一文,TIMSK寄存器可参考“基于ATMega16的数码管动态扫描显示实例”一文。另外,本例还使用了RAM空间的3个字节,分别作为时、分、秒的存储单元。
先来看TCNT0寄存器和TCNT2寄存器,它们分别是定时器T0和T2的计数寄存器,其结构完全一样,这里就只给出TCNT0的具体结构,如下表所示。
定时器T0和T2的计数位宽均为8位,初始值为0,最大计数值为255。要改变它们的初始计数值,直接写寄存器即可。
再来看TCCR0寄存器,它是定时器T0的控制寄存器,初始值为全0,具体如下表所示。
T0的工作模式由第3、6两位(WGM01 、WGM00)来决定,这里需要它们都为0,即让T0工作在普通定时模式。低3位(CS00~CS02)用来确定时钟的分频情况,它与定时器T1中的CS10~CS12完全一样,具体可参考“基于ATMega16的数码管动态扫描显示实例”一文中的相关部分,这里就不在重复了。
接下来看TCCR2寄存器,它是定时器T2的控制寄存器,初始值为全0,具体如下表所示。
T2的工作模式由第3、6两位(WGM21 、WGM20)来决定,这里需要它们都为0,即让T2工作在普通定时模式。低3位(CS20~CS22)用来确定时钟的分频情况,这里与T0、T1中的不完全一样,具体如下表所示。
在本例中,由于T2采用异步晶振的方式,晶振频率为32768Hz,所以上面3位设置为101,即选择128分频,分频后的频率为256Hz。这样在T2计数256次之后,就可以产生精确的秒信号。
下面来看ASSR寄存器,它是异步状态寄存器,为定时器T2所特有,初始值为全0,具体如下表所示。
第3位为异步设定位,写0时为系统时钟模式,写1时为异步时钟模式。在异步模式下,由于与系统时钟有差异,所以在操作TCCR2、OCR2、TCNT2等寄存器时,需要等待。第0~2位分别代表相应寄存器的更新忙标志位,只有等待值变为0时,才能对相应的寄存器进行操作。
最后看TIFR寄存器,它是中断标志寄存器,为T0、T1和T2所共有,初始值为全0,具体如下表所示。
其中的第0、2、6三位分别用来标志T0、T1、T2的溢出中断,即当定时器有溢出中断发生时,相应的位会被硬件置1 ,中断响应后会自动清零,写1将强制清零该位。
本例中一共使用到了27种指令,其中的JMP、RJMP、LDI、OUT、SER、SEC、ROL、BRNE等8条指令可参考“基于ATMega16的流水灯实例”一文。 NOP、RETI、CLR、SEI、INC、ADD、LPM、BRCC等8条指令可参考基于ATMega16的数码管动态扫描显示实例“”,其余11条指令解释如下。
1)寄存器数据直接送SRAM
STS k,Rr 0 ≤ r ≤ 31,0 ≤ k ≤ 65535
说明:将寄存器中的内容直接存储到数据存储空间中。对于带有SRAM的芯片,数据空间由寄存器堆,I/O 存储器和内部SRAM 存储器组成。对存储器数据的访问被限定在当前数据段的64K 字节的空间。该指令不一定支持所有的AVR芯片。
操作:(k) ← Rr PC ← PC + 2 32位机器码:1001 001d dddd 0000 kkkk kkkk kkkk kkkk
2)I/O空间数据送寄存器
IN Rd, A 0 ≤ d ≤ 31,0 ≤ A ≤ 63
说明:将I/O空间的数据传送到寄存器Rd中。
操作:Rd ← I/O(A) PC ← PC + 1 16位机器码:1011 0AAd dddd AAAA
3)“或”立即数
ORI Rd, K 16 ≤ d ≤ 31,0 ≤ K ≤ 255
说明:将寄存器Rd的值与常量进行“或”操作,结果送入寄存器Rd中。
操作:Rd ← Rd Ⅴ K PC ← PC + 1 16位机器码:0110 KKKK dddd KKKK
4)与立即数比较
CPI Rd, K 16 ≤ d ≤ 31,0 ≤ K ≤ 255
说明:完成寄存器Rd和常数的比较操作,寄存器的内容不改变,该指令后能使用所有条件跳转指令。
操作:Rd — K PC ← PC + 1 16位机器码:0011 KKKK dddd KKKK
5)SRAM数据直接送寄存器
LDS Rd,k 0 ≤ d ≤ 31,0 ≤ k ≤ 65535
说明:从数据区中指定的位置装入一个字节的数据到寄存器。对于带有SRAM的芯片,数据空间由寄存器堆,I/O 存储器和内部SRAM 存储器组成。对存储器数据的访问被限定在当前数据段的64K 字节的空间。该指令不一定支持所有的AVR芯片。
操作:Rr ← (k) PC ← PC + 2 32位机器码:1001 000d dddd 0000 kkkk kkkk kkkk kkkk
6)相等跳转
BREQ k -64 ≤ k ≤ 63
说明:条件相对跳转,测试零标志位Z,如果Z位被置位,则相对PC值跳转k个字。如果在执行CP、CPI、SUB 或SUBI 指令后,立即执行该指令,且当寄存器Rd中数与寄存器 Rr中数相等时,将发生跳转。可跳转k个字,k 为7位带符号数,最多可向前跳63个字,向后跳64个字。这条指令相当于指令“BRBS 1,k”。
操作:If Rd = Rr (Z = 1) then PC ← PC + k + 1, else PC ← PC + 1 16位机器码:1111 00kk kkkk k001
7)半进位标志为1跳转
BRHS k -64 ≤ k ≤ 63
说明:条件相对跳转,测试半进位标志H,如果H位被置位,则相对PC值跳转k个字。k 为7位带符号数,最多可向前跳63个字,向后跳64个字。该指令相当于指令“BRBS 5,k”。
操作:If H = 1 then PC ← PC + k + 1, else PC ← PC + 1 16位机器码:1111 00kk kkkk k101
8)减立即数
SUBI Rd, K 16 ≤ d ≤ 31,0 ≤ K ≤ 255
说明:寄存器Rd的内容和常数相减,结果送目的寄存器Rd。该指令工作于寄存器R16~R31之间,非常适合X、Y 和Z指针的操作。
操作:Rd ← Rd - K PC ← PC + 1 16位机器码:0101 KKKK dddd KKKK
9)寄存器位为1跳行
SBRS Rr,b 0 ≤ r ≤ 31,0 ≤ b ≤ 7
说明:测试寄存器的某一位,如果这一位为1则跳过下一条指令。
操作:If Rr(b) = 1 then PC ← PC + 2 (or 3) else PC ← PC + 1 16位机器码:1111 111r rrrr 0bbb
10)“与”立即数
ANDI Rd, K 16 ≤ d ≤ 31,0 ≤ K ≤ 255
说明:寄存器Rd的内容和常数逻辑与,结果送目的寄存器Rd。
操作:Rd ← Rd · K PC ← PC + 1 16位机器码:0111 KKKK dddd KKKK
11)寄存器半字节交换
SWAP Rd 0 ≤ d ≤ 31
说明:将一个寄存器中的高四位与低四位进行交换。
操作:Rd(7:4) ← Rd(3:0),Rd(3:0) ← Rd(7:4) PC ← PC + 1 16位机器码:1001 010d dddd 0010
此外,程序中还用到了另外一些伪指令(.INCLUDE、.DEF、.ORG可参考“基于ATMega16的流水灯实例”一文,.CSEG、.DB可参考“基于ATMega16的数码管动态扫描显示实例”一文),具体解释如下。
1)声明数据段(SRAM)
语法:.DSEG
说明:DSEG伪指令声明数据段的起始。一个汇编程序文件可以包含几个数据段,这些数据段在汇编过程中被连接成一个数据段。在数据段中,通常仅由BYTE伪指令(和标号)组成。每个数据段内部都有自己的字节定位计数器。可使用ORG伪指令定义该字节定位计数器的初始值,作为数据段在SRAM中的起始位置。DSEG伪指令不带参数。
2)定义标识符常量
语法:.EQU 标号 = 表达式
EQU伪指令将表达式的值赋给一个标识符,该标识符为一个常量标识符,可以用于后面的指令表达式中,在汇编时凡遇到该标识符都以其等值表达式替代。在编写程序中,只要修改此表达式,就修改了程序中多处涉及该表达式的地方,减少了程序的修改量。但该标识符的值不能改变或重新定义。
下面对程序中的相关部分进行一下说明。
1)ATMega16可以让定时器T2工作在异步模式下,即工作在低频晶振模式下。一般在引脚PC6和PC7之间接一个32768Hz的时钟晶振,让T2在异步模式下使用。在T2中选择时钟的128分频,然后让计数器计满256次后产生溢出中断,则此时T2的中断时间刚好就是1秒。这样做的好处在于,避免在使用较高频率系统晶振的情况下,通过8位定时器难以精确地实现较长时间的定时。
2)定时器T2工作在异步模式时,对其寄存器的操作由于处于不同的时钟频率之下,所以需要等待。即在操作完TCCR2、OCR2、TCNT2等寄存器之后,需要等待他们完成。或者说在操作他们之前需要判忙,不忙才能进行操作。另外,为了保险,在配置完成T2之后,还需要强制清一下T2的中断标志位再使用。
3)有了秒时钟之后,再产生出分和时,并在SRAM中开辟三个字节空间,分别用于存放实时的时、分、秒。申请的空间最好位于SRAM的最低地址处(因为最高地址被用作了堆栈空间)。在ATMega16中,数据空间一共包含了三类空间,地址从低到高依次是通用寄存器空间、I/O寄存器空间和内部SRAM空间。而SRAM空间是从地址$0060开始到地址$045F结束,所以本例中申请的时、分、秒空间被指定在地址$62~$60处。
4)定时器T2产生出的时、分、秒等计时信号的值,需要作BCD调整,以适应时钟的计时规则。由于时、分、秒最大都只有两位数,且十位的最大值只到5,所以只需要对个位进行调整即可。调整原理是这样,判定个数位在加1后是否大于10(十六进制$A),若不大于则取实际的值,若大于则减去一个十六进制数$FA,这样所得结果就调整过来了。比如,假设当前秒的值为十六进制的$09,则在加1后为十六进制的$0A,而非想要的$10。这时进行$0A-$FA的操作,其结果的最后两位就是$10(前面的若干个F表示负数,不用管它)。然后再把$10回存到秒的存储单元(SRAM中地址为$60处)中,以后取出来再加1,就又回到个位数不超过10的操作了,如此循环。所谓的BCD调整,其实就是让十六进制数“看起来”像十进制数一样的操作。虽然本质上还是十六进制,但“看上去”就像是十进制一样。
5)在分和秒的边界(最大值)判定上,由于是在加1之后BCD调整之前,所以对比的值就是真实的十六进制数,即在判断是否大于60时,并不是与$60比较,而是与$5A比较(因为BCD调整前,$59+1=$5A,调整后才为$60)。但时的边界判定就不存在此问题了(因为$23+1=$24)。
6)本例的动态扫描原理与“基于ATMega16的数码管动态扫描显示实例”一文中的一样,只不过把所使用的定时器换成了T0。这里要强调的是,如何把时间的个位和十位拆分开来显示。在本例中,当取低4位(个位数)时,直接把高4位屏蔽。取高4位(十位数)时,先把高低4位交换,再屏蔽高4位。