51单片机出租车计价器(汇编语言)
要求
- 使用信号发生器作为模拟出租车轮胎转速
- 使用数码管可显示费用,里程,速度
- 按钮按下开始计费
- 按钮抬起后停止计费
- 按钮再次按下后清零
- 里程达到2km前费用均为8元
- 里程达到2km后每千米2.6元
- 代码部分使用汇编语言完成,不可使用c语言编译
效果图
- 里程达到2km前费用均为8元
- 里程达到2km后每公里2.6元
- 按钮抬起后里程不再增加
- 按钮再次按下后清零
理论基础
指令系统
数据传送: 寄存器寻址,寄存器间接寻址等
中断: 外部中断
89C51单片机定时器的4种模式及其应用
实现思路
数码管显示
要实现数码管的显示,需要同时输出段码和位码才能让数码管显示
位码
控制数码管的显示位
段码
控制数码管的具体显示
共阳极段码表
DB 0C0H, 0F9H, 0A4H, 0B0H, 99H, 92H, 82H, 0F8H, 80H, 90H //[0~9]
DB 88H, 83H, 0C6H, 0A1H, 86H, 8EH //[A~F]
DB 89H, 0C7H, 8CH, 0C1H, 91H, 0FFH //[H L P U Y 灭]
这里建议在程序调试初期使用0-F的16进制段码表
实例:
MOV P2,#00H ;位码
MOV P1,#0C0H ;段码
此时数码管的第1位会显示为数字0
定时刷新
本程序中需要12位数码管全部显示,所以需要定时刷新所有数码管
而刷新程序周期为5ms (下面会解释)
如果一次只刷新1位会导致显示不稳定
(即每次自增显示位,然后与某数ANL)
Display:
LCALL Delay
MOV A, cDisplayBit
MOV P2, A
MOV DPTR, #DisplayTable1
MOV A, #cDisplayBuffer
ADD A, cDisplayBit
MOV R0, A
MOV A, @R0
MOVC A, @A+DPTR
MOV P1, A
INC cDisplayBit
ANL cDisplayBit,#0FH
以上程序调用一次只会刷新一个数码管,刷新范围0-15位(0FH)
所以需要一次全部刷新所有数码管
Display:
MOV R5,#0CH ;需要扫描12个位
D1:
LCALL Delay
MOV A, cDisplayBit
MOV P2, A
-------------------------------------;译码
MOV DPTR, #DisplayTable1
MOV A, #cDisplayBuffer
ADD A, cDisplayBit
MOV R0, A
MOV A, @R0
MOVC A, @A+DPTR
MOV P1, A
--------------------------------------
INC cDisplayBit
DJNZ R5,D1
MOV cDisplayBit,#00H ;自增,清零
RET
在本程序中设置了两个显示缓冲区
cDisplayBuffer 存储单字节BCD码 如 08H 07H 有12位
BCD 存储双字节BCD码 如 12H 60H 有6位
BCD送入cDisplayBuffer的程序
LOOSE:
MOV R5,#06H ;总共5个字节,需要循环5次
MOV R0,#BCD ;6位双字节首地址
MOV R1,#cDisplayBuffer ;12位单字节缓冲区地址
LOOP1:
MOV A,@R0
ANL A,#0F0H ;取高位
SWAP A
MOV @R1,A
INC R1
MOV A,@R0 ;存高位
ANL A,#0FH ;取低位
MOV @R1,A ;存低位
INC R0
INC R1
DJNZ R5,LOOP1
RET
小数点显示
带小数点的共阳极段码表
DB 0BFH, 86H, 0DBH, 0CFH, 0E6H, 0EDH, 0FDH, 87H, 0FFH, 0EFH
只需要在显示程序后追加一个程序,指定段码,使用同样的BCD译码进行叠加显示即可看到小数点
LCALL Delay
MOV P2, #01H
MOV DPTR, #DisplayTable2
MOV A, #cDisplayBuffer
ADD A, #01H
MOV R0, A
MOV A, @R0
MOVC A, @A+DPTR
MOV P1, A
LCALL Delay
MOV P2, #05H
MOV DPTR, #DisplayTable2
MOV A, #cDisplayBuffer
ADD A, #05H
MOV R0, A
MOV A, @R0
MOVC A, @A+DPTR
MOV P1, A
LCALL Delay
MOV P2, #0AH
MOV DPTR, #DisplayTable2
MOV A, #cDisplayBuffer
ADD A, #0AH
MOV R0, A
MOV A, @R0
MOVC A, @A+DPTR
MOV P1, A
RET
Delay为防抖程序
DisplayTable2为带小数点的共阳极段码表
如果不想写的这么冗杂也可以直接置小数点
LCALL Delay
MOV P2, #01H
MOV P1,#80H
LCALL Delay
MOV P2, #05H
MOV P1,#80H
LCALL Delay
MOV P2, #0AH
MOV P1,#80H
80H为共阳极 “.” 的段码
计速
频率捕捉
速度需要显示为1s内平均速度,我们要先捕捉到信号发生器的频率再进行计算操作
所以我们需要使用到89C51单片机的两个计时器的两个功能
本程序中T0计时器使用边缘中断模式
T1计时器使用计时器模式,产生5ms的定时程序
在程序开头需要指明中断程序的入口地址
ORG 0000H
SJMP MAIN
ORG 0003H
LJMP 外部中断
ORG 001BH
LJMP 定时器
T0对应的中断子程序,每次对速度自增,1s后取值清零实现频率捕捉
INC cache ;速度所在地址单元
RETI
T1对应的中断子程序
为了实现1s的计时需要使用到一个Temp标志位,
每次自增,在MAIN程序中判断是否达到200即可实现1s的定时功能
定时5ms的中断程序
该单片机晶振为22.1184MHz
计算过程略
MOV TH1,#0DCH
MOV TL1,#00H
INC cTemp
RETI
部分主程序
M1:
MOV A,cTemp ;5ms定时,计200次为1秒
XRL A,#200
JNZ M1 ;不满200继续循环
LCALL 速度计算
MOV cTemp,#0
MOV cache,#0 ;速度置零
SJMP M1 ;死循环
速度计算
轮胎每秒转1圈折合km/h为
1 r/s = 6.588 km/h
这里需要精确显示,所以需要使用到双字节乘法和双字节译码程序
51单片机的MUL指令是针对16进制数的,不要天真的以为是10进制数乘法
双字节乘法例子
12 34H * 56 78H = 06 26 00 60H
结果有4个16进制数,存放于4个地址中
双字节译码程序
R0是待译码双字节数高位地址
R1是译码结果的高字节地址
调用前先将R0和R1寄存器赋值
BinDec: ;双字节BCD译码
CLR A
MOV @R1,A
INC R1
MOV @R1,A
INC R1
MOV @R1,A
PUSH 7
MOV R7,#16
BD1:
CLR C
INC R0
MOV A,@R0
RLC A
MOV @R0,A
DEC R0
MOV A,@R0
RLC A
MOV @R0,A
PUSH 1
MOV A,@R1
ADDC A,@R1
DA A
MOV @R1,A
DEC R1
MOV A,@R1
ADDC A,@R1
DA A
MOV @R1,A
DEC R1
MOV A,@R1
ADDC A,@R1
DA A
MOV @R1,A
POP 1
DJNZ R7,BD1
POP 7
RET
@Ri表示寄存器内地址位所储存的16进制数
译码前
@R0: 01H @R0+1:01H
译码后R0会被清零所以需要和数据缓存位分开
译码后
@R0: 00H @R0+1:00H
@R1: 00 @R1+1:02 @R1+2:57
所以速度计算的具体思路就是使用双字节乘法
每秒转过的圈数 * 6.588 即可
由于小数点的影响我们有以下几种方案
先假设频率是10,速度65.8km/h
6588 * 10 = 06 58 80
嗯,看起来很简单
看一下16进制
16 BCH * 0AH = 01 01 58
哦豁完蛋,溢出了,译码程序只能够译码2位16进制数
01 01H = 0257
01 58H = 0344
和原来的数完全不沾边
那么就需要一点骚操作了
10进制
65 * 10 = 06 50
16进制
41H * 0AH = 02 8A H
缺了后面的8但已经大差不差
那我们能不给强行补上呢
事实上是可以的,而且还挺准的
0E0H * 0AH = 08 C0H
然后取高位加到前面的低位上去
02 8AH
08H
02 92H = 06 58
锵锵,6588就出现了
你问我E0咋来的?
猜的(真切)
在调试的时候我已经把译码程序都加上去了,然后实在无奈了
对着低位一波操作结果发现数字对了,甚至还很准确
再把小数点加上就很nice了
效果图
(谁家出租车能跑到600多 23333~~)
计程
里程追加
如题所示,在多次使用乘法之后我放弃了
难点
183 * 1000 = 18 30 00
早就溢出了,这才转了1000圈,才1.83km
而且需要储存走过的圈数至少需要2位16进制位才够用
这样译码器负担更重
即使是
1 * 1000
8 * 1000
3 * 1000
也还是存在问题
如果带上一个系数,比如说
每转10圈再对路程进行更新
然而该方法我尝试过同样也行不通
寄
所以我最后采用的方案是进行追加操作
使用4个地址位来作为我的缓存位
前两位可以直接接到显示缓冲区
00 00 00 00
18 30
使用DA和ADDC命令便可轻松做到
DA 命令可以简单理解为 出现字母的数转化为10进制数
MOV A,#0AH
DA A
A的值为 10H
MOV A,#10H
DA A
A的值同样是10H
而当A大于100后使用DA命令会将C置1
对高位进行ADDC 高位,#00H操作即可实现进位
程序较为简单,这里不给出了
计价
里程判断
需要对里程进行判断,小于等于2km时不对价格进行追加
这里我选用的方案是
减2后判断是否出现进位
若没有出现则退出并置数码管为8
MOV A,BCD+2
CLR C
SUBB A,#02H ;判断是否大于2
JC PRET ;小于2直接置8然后退出
PRET:
MOV BCD+2,#08H
RET
价格追加
和里程追加同理
只是这里每一圈追加的数是
26 * 183 = 4758
同样也需要4个缓冲位
小数点自己处理一下
按钮功能
抬起后价格不增加
在外部中断程序中加入JB对按钮进行监听
速度显示还是得放外面,不然成0去了
只要信号发生器还开着速度就不会是0
INC cache ;speed
JB P3.7,EXIT
LCALL 路程
LCALL 价格
RETI
再次按下后清零
这里需要涉及到
循环中只进行一次的操作
回想到高级程序语言,我们可以这样
mark = 1 标志位置1
while(1):
if mark == 1:
只运行一次的程序
mark = 0 标志位置0
循环程序
这样就实现了每次按下按钮之后都对地址进行一次清零
最终的外部中断程序修改如下
外部中断:
INC cache ;speed
JB P3.7,EXIT ;按下放通,抬起就置mark为0
LCALL 里程
LCALL 价格
MOV A,mark ;取mark
XRL A,#1 ;判断mark
JZ 清零 ;清零内存
RETI
EXIT:
MOV mark,#01H ;mark置1
RETI
这里给个清零程序
(直接FF干光算了,其实到80H就可以了)
清零:
MOV mark,#00H ;mark清零
MOV R1,#0FFH
MOV R0,#20H ;地址清零
C1:
MOV @R0,#00H
INC R0
DJNZ R1,C1
RETI
汇编源代码
ORG 0000H
SJMP MAIN
ORG 0003H
LJMP TRIGGER ;外部中断
ORG 001BH
LJMP TIMER ;定时器
DisplayTable1: DB 3FH,06H,5BH,4FH,66H,6DH,7DH,07H,7FH,6FH,77H,7CH,39H,5EH,79H,71H,76H,38H,73H,3EH,6EH,00H
DisplayTable2: DB 0BFH, 86H, 0DBH, 0CFH, 0E6H, 0EDH, 0FDH, 87H, 0FFH, 0EFH
cDisplayBuffer EQU 20H ;12位段码实际值的缓冲区 12位
cDisplayBit EQU 2CH
cTemp EQU 2DH
path EQU 2FH
mark EQU 6AH
cache EQU 36H
result EQU 75H ;运算结果
BIN EQU 46H ;译码器译码后的BCD
BCD EQU 40H ;40-45 6个BCD位
MAIN:
MOV cache,#00H
MOV cTemp,#00H
MOV TMOD,#10H
MOV TH1,#0DCH
MOV TL1,#00H
SETB ET1
SETB TR1
SETB IT0
SETB EX0
SETB EA
M1:
MOV A,cTemp ;5ms定时,计200次为1秒
XRL A,#200
JNZ M1
LCALL SPEEDCOUNT
MOV cTemp,#0
MOV cache,#0 ;速度置零
SJMP M1 ;死循环
;0 按下,1抬起
TRIGGER:
INC cache ;speed
JB P3.7,EXIT ;按下放通,抬起就置mark为0
LCALL PATHINC
LCALL PRICEINC
MOV A,mark ;取mark
XRL A,#1 ;判断mark
JZ CCLR ;清零内存
RETI
TIMER: ;5ms计时器
MOV TH1,#0DCH
MOV TL1,#00H
LCALL SHOW
INC cTemp
RETI
SPEEDCOUNT:
MOV A,cache
MOV B,#0E2H
MUL AB
MOV result+1,B
MOV B,#41H
MOV A,cache
MUL AB
ADD A,result+1
MOV result+1,A
MOV A,B
ADDC A,#0
MOV result,A
MOV R0,#result
MOV R1,#BIN
LCALL BinDec
MOV BCD+5,BIN+2 ;L
MOV BCD+4,BIN+1 ;M 00 06 58
RET
BinDec: ;双字节BCD译码
CLR A
MOV @R1,A
INC R1
MOV @R1,A
INC R1
MOV @R1,A
PUSH 7
MOV R7,#16
BD1:
CLR C
INC R0
MOV A,@R0
RLC A
MOV @R0,A
DEC R0
MOV A,@R0
RLC A
MOV @R0,A
PUSH 1
MOV A,@R1
ADDC A,@R1
DA A
MOV @R1,A
DEC R1
MOV A,@R1
ADDC A,@R1
DA A
MOV @R1,A
DEC R1
MOV A,@R1
ADDC A,@R1
DA A
MOV @R1,A
POP 1
DJNZ R7,BD1
POP 7
RET
PRET:
MOV BCD,#08H ;2km内直接置8返回
RET
TORET:
RET
EXIT:
MOV mark,#01H ;mark置1
RETI
CCLR:
MOV mark,#00H ;mark清零
MOV R1,#0FFH
MOV R0,#20H ;地址清零
C1:
MOV @R0,#00H
INC R0
DJNZ R1,C1
RETI
PATHINC:
CLR C
MOV A,#30H ;L
ADDC A,cache+2
DA A
MOV cache+2,A
MOV A,#18H ;H
ADDC A,cache+1
DA A
MOV cache+1,A
MOV A,BCD+3
ADDC A,#0
DA A
MOV BCD+3,A
MOV A,BCD+2
ADDC A,#0
DA A
MOV BCD+2,A
RET
PRICEINC:
JB P3.7,TORET ;检测按钮
MOV A,BCD+2
CLR C
SUBB A,#02H ;判断是否大于2
JC PRET ;小于2直接置8然后退出
MOV A,#58H ;L
ADD A,cache+4 ;L
DA A
MOV cache+4,A
MOV A,#47H
ADDC A,cache+3
DA A
MOV cache+3,A
MOV A,#00H
ADDC A,BCD+1
DA A
MOV BCD+1,A
MOV A,#00H
ADDC A,BCD
DA A
MOV BCD,A
RET
SHOW:
LCALL LOOSE
LCALL Display
RET
LOOSE:
MOV R5,#06H ;总共6个字节,需要循环6次
MOV R0,#BCD ;时间序列存储首地址
MOV R1,#cDisplayBuffer ;12位缓冲区
LOOP1:
MOV A,@R0
ANL A,#0F0H ;取高位
SWAP A
MOV @R1,A
INC R1
MOV A,@R0 ;存高位
ANL A,#0FH ;取低位
MOV @R1,A ;存低位
INC R0
INC R1
DJNZ R5,LOOP1
RET
Display: ;通用显示程序
MOV R5,#0CH
D1:
LCALL Delay
MOV A, cDisplayBit
MOV P2, A
MOV DPTR, #DisplayTable1
MOV A, #cDisplayBuffer
ADD A, cDisplayBit
MOV R0, A
MOV A, @R0
MOVC A, @A+DPTR
MOV P1, A
INC cDisplayBit
DJNZ R5,D1
MOV cDisplayBit,#00H
D2:
LCALL Delay
MOV P2, #01H
MOV P1,#80H
LCALL Delay
MOV P2, #05H
MOV P1,#80H
LCALL Delay
MOV P2, #0AH
MOV P1,#80H
RET
Delay: ;防抖
MOV R0,#10
MOV R1,#10
DJNZ R1,$
DJNZ R0,$-4
RET
END
翻译比较生硬,结构不够优雅
仿真文件
后记
本计价器还有多种实现思路,这里就不作叙述了
希望大家能够有所收获
参考了这篇文章
基于MCS-51单片机使用定时器编写时钟程序(汇编)