C51 矩阵键盘+数码管 汇编功能实现
proteus仿真电路图
对应题目要求搭建。其中数码管的位选出加了三极管反相,是因为proteus里的这个四位数码管的位选是低电平驱动,而实验室里的真正的数码管是高电平驱动。
功能1
记录按键次数:启动时数码管显示“0”,按一下键盘数码管显示自动加一。
先将完整代码附上,之后详细进行讲解。
/*
made by xx, xyz's bf
*/
ORG 0000H
AJMP MAIN
ORG 0100H
MAIN:
MOV R3,#00H
MOV DPTR,#Table
LOOP: //数码管动态扫描
MOV A,R3
MOV R4,A
MOV P1,#08H
LCALL DISP
MOV P1,#04H
LCALL DISP
MOV P1,#02H
LCALL DISP
AJMP SCAN
DISP: //数码管每位的数的显示,写成子函数的原因是使代码简洁
MOV A,R4
MOV B,#10
DIV AB
MOV R4,A
MOV A,B
MOVC A,@A+DPTR
MOV P0,A
ACALL D10MS
MOV P1,#00H
RET
SCAN: //矩阵键盘扫描
MOV P2,#0FH //控制列的四位端口输出置为低
MOV A,P2
ANL A,#0FH
CJNE A,#0FH,KCODE
AJMP LOOP
KCODE: //扫描到按键按下,显示的数+1
INC R3
XIAODOU: //松手消抖
MOV A,P2
ANL A,#0FH
CJNE A,#0FH,XIAODOU
AJMP LOOP
D10MS: //延时10ms函数
L3: MOV R6,#25
L1: MOV R7,#250
L2: DJNZ R7,L2
DJNZ R6,L1
RET
Table: DB 03FH,06H,05BH,04FH,66H,6DH,7DH,07H,7FH,6FH //数码管显示0~9
END
延时函数和Table
首先先说一下最简单的,延时函数D10ms和Table,应该接触过C51汇编实现的都会知道这两部分,这里就简单说一下延时函数的原理,这里就是相当于C语言中的2个嵌套的for循环。外面的循环25次,内部的循环250次,这样就相当于总共执行的25×250=6250次DJNZ(如果不知道DJNZ是什么,可以去网上或文档里搜索),且执行一次DJNZ的机器周期为2,所以总共经历了6250×2=12500个机器周期。
而C51的时钟周期为11.0592MHz,我们这里把它认为是12MHz,一个机器周期包含12个时钟周期,所以一个机器周期为:
所以执行一次延时函数D10ms所用的时间为:
当然这里完全可以更精确些,但是因为本次程序对延时时间的要求不需要那么精准,所以没有更改。
数码管显示
MAIN:
MOV R3,#00H //R3为数码管要显示的数值,在这里先初始化为0
MOV DPTR,#Table //将Table数组的首地址放在16位寄存器DPTR中
LOOP: //数码管动态扫描
MOV A,R3 //这两句代码的作用是使个位也可以调用DISP函数
MOV R4,A
MOV P1,#08H //数码管位选: 个位
LCALL DISP //调用DISP子函数
MOV P1,#04H //数码管位选:十位
LCALL DISP
MOV P1,#02H //数码管位选:百位
LCALL DISP
AJMP SCAN //无条件跳转到SCAN键盘扫描函数
DISP: //数码管每位的数的显示,写成子函数的原因是使代码简洁,否则得重复三遍
MOV A,R4
MOV B,#10
DIV AB //A除以10,商放在了A中,余数放在了B中
MOV R4,A
MOV A,B
MOVC A,@A+DPTR //Table的首地址加上偏移地址之后的地址里的内容赋给A,这个内容就是要显示的数
MOV P0,A //把A赋给P0即可进行显示,P0口连接着数码管的段选
ACALL D10MS //延时10秒
MOV P1,#00H //把数码管位选口关上,和延时的目的都是为了程序的稳定正常运行
RET
R0-R7都是C51汇编中的寄存器,可以拿来作为变量使用(一般使用R2-R7)。在这里使用了R3作为递增变量(在一开始初始化为0),R4是中间变量。R3是要显示的数(后面会提)。第一次R3整除10,余数就是要显示的个位,商放在R4中;第二次用R4整除10,余数就是要显示的十位,商放在R4中。然后再执行一次,此时的余数就是要显示的百位。
因为R3寄存器是8位的,所以它的范围为: 0-255。
关于程序MOVC A,@A+DPTR的易懂的解释(来源:网络):
本指令是将ROM中的数送入A中。本指令也被称为单片机查表指令,常用此指令来查一个已做好在ROM中的表格。
此条指令引出一个新的寻址办法:变址寻址。本指令是要在ROM的一个地址单元中找出数据,显然必须知道这个单元的地址,这个单元的地址是这样确定的:在执行本指令立脚点DPTR中有一个数,A中有一个数,执行指令时,将A和DPTR中的数加起为,就成为要查找的单元的地址。
查找到的结果被放在A中,因此,本条指令执行前后,A中的值不一定相同。
例:有一个数在R0中,要求用查表的办法确定它的平方值(此数的取值范围是0-5)
MOV DPTR,#TABLE MOV A,R0 MOVC A,@A+DPTR TABLE: DB 0,1,4,9,16,25
设R0中的值为2,送入A中,而DPTR中的值则为TABLE,则最终确定的ROM单元的地址就是TABLE+2,也就是到这个单元中去取数,取到的是4,显然它正是2的平方。其它数据也能类推。
矩阵键盘扫描
P2.0-P2.3控制矩阵键盘的行,P2.4-P2.7控制矩阵键盘的列。
扫描的原理是先令高四位为低电平,即控制列的四个端口输出低电平,此时如果四个控制行的管脚有输出低电平
的,则意味着有按键被按下了。因为本次的功能是只检测是否有按键被按下,而不用检测具体哪个按键被按下了,所以会稍微简单一点,检测具体的按键的功能在第二个题目中有要求,所以后面再提。
SCAN: //矩阵键盘扫描
MOV P2,#0FH //控制列的四位端口输出置为低
MOV A,P2 //P2的电平输出到A中
ANL A,#0FH //将A的数与0FH相与,如果低四位有低电平,那么A将和0FH不相等
CJNE A,#0FH,KCODE //如果A和0FH不相等,意味着有按键被按下,跳转至KCODE
AJMP LOOP //无条件跳转至LOOP,重新开始循环
KCODE:
INC R3 //扫描到按键按下,显示的数+1
XIAODOU: //松手消抖
MOV A,P2
ANL A,#0FH
CJNE A,#0FH,XIAODOU
AJMP LOOP
现在说一下松手消抖的原理,可以很明显看到松手消抖子函数的代码与SCAN中的几行非常相似。它的原理就是检测控制矩阵键盘的四行的P2口低四位是否还有低电平,如果有,说明键盘还在被按着,进入死循环。否则退出,跳转至LOOP重新进行循环。
仿真情况
功能2
显示哪个键被按下:启动时数码管无显示,按下键盘,数码管显示该键盘所在的行列值。
完整代码为:
/*
made by xx, xyz's bf
*/
ORG 0000H
AJMP MAIN
ORG 0100H
MAIN:
MOV DPTR,#Table
MOV R3,#0FEH
START: //加一个START的目的是要求说一开始不能有显示,
CJNE R4,#0,LOOP //如果不加的话一开始会显示0
MOV P0,#00H
AJMP SCAN
LOOP: //数码管动态扫描循环
MOV A,R4
MOV P1,#04H
MOVC A,@A+DPTR
MOV P0,A
LCALL D20MS
MOV P0,#0
MOV A,R5
MOV P1,#08H
MOVC A,@A+DPTR
MOV P0,A
LCALL D20MS
MOV P0,#0
SCAN: //键盘扫描
MOV A,R3
MOV P2,A
MOV A,P2
ANL A,#0F0H //将#0FH与A相与,结果放在A中
CJNE A,#0F0H,NEXT //如果不相等,代表有按键按下
SETB C
MOV A,R3
RLC A
MOV R3,A
CJNE A,#0EFH,START //还没扫描完一遍,继续扫
AJMP MAIN //四行都检测完一遍,跳转至MAIN重新循环
NEXT: //已经检测到了按键按下,输出按键的行和列的位置
SWAP A
MOV R2,A
LCALL COMP
MOV R5,A
MOV A,R3
MOV R2,A
LCALL COMP
MOV R4,A
AJMP XIAODOU
COMP: //计算按下的按键的行数和列数
MOV A,R2
ANL A,#01H
JZ C1
MOV A,R2
ANL A,#02H
JZ C2
MOV A,R2
ANL A,#04H
JZ C3
MOV A,R2
ANL A,#08H
JZ C4
C1: MOV A,#1
RET
C2: MOV A,#2
RET
C3: MOV A,#3
RET
C4: MOV A,#4
RET
XIAODOU: //松手消抖
MOV A,P2
ANL A,#0F0H
CJNE A,#0F0H,XIAODOU
AJMP START
D20MS: //延时20ms函数
L3: MOV R6,#50
L1: MOV R7,#250
L2: DJNZ R7,L2
DJNZ R6,L1
RET
Table: DB 03FH,06H,05BH,04FH,66H,6DH,7DH,07H,7FH,6FH //数码管显示0~9
END
延时函数和Table数组通要求一,在这里不再讲解。
数码管显示
因为此次显示的是所按按键的行数和列数,所以只用到了两位数码管。
LOOP: //数码管动态扫描循环
MOV A,R4 //R4中为所按按键的行数
MOV P1,#04H //数码管位选:显示行数的位
MOVC A,@A+DPTR //要求一中有详细解释
MOV P0,A
LCALL D20MS //延时20ms
MOV P0,#0 //关闭位选,和延时函数的功能一样:保证程序稳定正常运行
MOV A,R5 //R5中为所按按键的列数
MOV P1,#08H //数码管位选:显示列数的位
MOVC A,@A+DPTR
MOV P0,A
LCALL D20MS //延时20ms
MOV P0,#0 //关闭位选,和延时函数的功能一样:保证程序稳定正常运行
本次的数码管显示要求是要比上一个简单,显示两个数:按键的行数和列数,而不像要求一一样显示一个百位的数,还需要先把百、十、个位分离才能进行显示。
矩阵键盘扫描
MOV R3,#0FEH //R3是用来输出给P2的,使P2的低四位(控制行的端口)轮流输出低电平,达到"扫描"的目的
//0FEH即代表最低位时低电平,即控制第一行的端口输出为低电平。当16进制最高位是字母时要加个0
SCAN: //键盘扫描
MOV A,R3
MOV P2,A //把R3赋给P2,对应端口输出低电平
MOV A,P2 //将P2电平返回,在对应行有按键按下,则P2的高四位中有低电平存在
ANL A,#0F0H //将#0FH与A相与,结果放在A中
CJNE A,#0F0H,NEXT //如果不相等,代表有按键按下
SETB C //C是进位标志位,这里指将溢出位置为高电平
MOV A,R3
RLC A //带进位标志位循环左移
MOV R3,A
CJNE A,#0EFH,START //还没扫描完一遍,继续扫
AJMP MAIN //四行都检测完一遍,跳转至MAIN重新循环
首先来讨论一下带进位标志位的循环左移问题:
举个例子,现在我们要循环左移一个数A=10000001,如果现在进位标志位时0的话,循环左移后的结果为00000010,最高位的1进入进位标志位,进位标志位中的0到达数A的最低位;同理,当进位标志位为1时,结果为00000011,进位标志位变为1。
现在回到矩阵键盘扫描的程序,一开始初始化R3为0FEH,即11111110,赋给口之后会使P2.0输出低电平,即控制第一行的端口输出低电平,来扫描第一行的按键有没有被按下,如果此时在第一行有被按下的按键,则高四位,即控制列的端口一定有电平为低的。例如,如果按下的按键是第一行第一列,现在R3=0FEH赋给了P2,则会导致控制第一列的端口P2.4也输出低电平。
如果第一行没有扫描到按键,则接下来会继续扫描第二行,这里就用到了循环左移,R3本来使FEH,即111111110,现在设进位标志位C=1, 则由上面描述的循环左移的原理可知,R3循环左移一次的结果是11111101, 即FDH,看出来了吗,这个R3=11111101赋给P2的话,就是检测第二行了!之后第三、四行同理。
之后的一句:
CJNE A,#0EFH,START //还没扫描完一遍,继续扫
意思就是当移位到11101111的时候,表示移位过头了,那么接下来就应该重头开始循环左移了呀。
计算行和列的位置
当我们检测到有按键按下之后,接下来要做的就是确定是哪个按键按下,即确定按下的按键的行和列数。
NEXT: //已经检测到了按键按下,输出按键的行和列的位置
SWAP A //A高四位和低四位位置互换,即:11010000→00001101
MOV R2,A //COMP函数的输入参数为R2
LCALL COMP //调用计算行数和列数的子函数
MOV R5,A //R5为计算得到的列数
MOV A,R3 //R3为我们自己设的用来按键扫描的数,控制行的低四位轮流为低电平,这里就是确定哪个为低电平
MOV R2,A //COMP函数的输入参数为R2
LCALL COMP //调用计算行数和列数的子函数
MOV R4,A //R4为计算得到的行数
AJMP XIAODOU //此部分程序结束,跳转至“消抖”部分
COMP: //计算按下的按键的行数和列数
MOV A,R2
ANL A,#01H //检测是否是第一行/列
JZ C1 //上一行相与的结果如果是0,则跳转至C1
MOV A,R2
ANL A,#02H //检测是否是第二行/列
JZ C2 //上一行相与的结果如果是0,则跳转至C2
MOV A,R2
ANL A,#04H //检测是否是第三行/列
JZ C3 //上一行相与的结果如果是0,则跳转至C3
MOV A,R2
ANL A,#08H //检测是否是第四行/列
JZ C4 //上一行相与的结果如果是0,则跳转至C4
C1: MOV A,#1 //行/列数是1
RET //退出子函数
C2: MOV A,#2 //行/列数是2
RET
C3: MOV A,#3 //行/列数是3
RET
C4: MOV A,#4 //行/列数是4
RET
对于这个部分,或许举个例子可以更容易理解。
假设我们按下的按键是第三行、第二列。那么键盘扫描至第三行,即R3=0FBH,即R3=11111011时,高四位中控制第二列的端口P2.5会为低电平。这部分一开始的A来自于SCAN函数第四行与0F0H相与后的A,即本来A是P2的电平值,在这里就是11011011,相与后就是11010000,SWAP高低四位互换后就成00001101,当这个数与02H相与后结果就是0,对应就会输出列数为2。
下面来确定行数。我们进行键盘扫描时轮流使控制每一行的端口输出低电平,即使R3循环左移(还记得嘛,不记得就往上翻翻),所以我们就可以R3来获取它的行数。当按下的按键在第三行时,R3=11111011,取反后就是R3=00000100,与04H相与后结果是0,对应就会输出行数为3。
这一部分可能有点绕,事实上我写程序的时候也绕晕了会儿哈哈,需要耐心来进行思考和研究哦。
一开始不能有显示
这一部分其实不难,我借助的是行数和列数,一开始没有按键按下去,则没有计算获取行数和列数,所以储存行数和列数的寄存器R4和R5都是0,当获取之后两个都不可能是0,因为行和列的范围都是1-4,借助这个信息就可以完成这个功能:
START:
CJNE R4,#0,LOOP //如果R4不是0,则进入LOOP数码管扫描循环
MOV P0,#00H
AJMP SCAN //R4是0,跳过数码管扫描循环,直接进入矩阵键盘扫描循环,所以数码管不显示
仿真情况
END