51单片机学习(汇编语言)

51单片机学习(汇编语言版)

注:本笔记参考疯狂的石头单片机——51篇 汇编版

一、点LED灯

我们先要看电路图(注意这个图和下面的电路板照片是反着的)

可以看到最上面是IO口,1对应P0的0口,8对应P0的7口,灯是绿色的,想要让灯点亮,下面VCC是正极高电平,上面的IO口应该为负极低电平,这样电路才能导通,我们想要让第一个灯即D8亮,就需要让P0的0口为低电平

1
2
3
4
MOV A,#11111110B  ;传送指令,传到A累加器,#代表立即数,即数据(B代表二进制),最后一位低电平,最后一盏灯亮。前面就是说存入预显示灯的位置数据给累加器A
MOV P0,A;把A的值传给P0口
JMP $;JMP是跳转指令,这里加个$意思是保持当前状态,意思就是程序运行到这里就停止
END;结束

img

这就是效果

我们可以改进一下,比如说把二进制改成十六进制

1
MOV A,#0FEH  ;这里需要注意FE代表11111110,H代表十六进制。如果最高位是0~9那么比如9EH可以直接写,但是如果是A~F那么前面必须加一个0才行,即0FEH,FEH错误

我们现在想要让高四位的四个灯全部点亮,低四位的四个灯不亮

1
MOV A,#0FH  ;这就是00001111

img

我们还可以把代码精简一下,不需要用到累加器A,直接把值传到P0口

1
2
3
MOV P0,#0F0H
JMP $
END

0F0H就是11110000即低四位的灯亮

img

我们还有另一种方法点亮灯

CLR是清除位的意思,就是清0寄存器

1
2
3
CLR P0.0    ;就是说把P0的0口进行清零
JMP $
END

img

再看一个

1
2
3
CLR P0.7
JMP $
END

这个的意思就是说把P0的7口清零

img

接下来我们试一试让灯间隔着亮,即一个亮一个不亮

1
2
3
MOV P0,#55h   ;后面的H是小写的h也是可以的
JMP $
END

img

其实上面的程序都不是一个完整的程序,那么什么样的程序是一个完整的程序呢?

1
2
3
4
5
6
ORG 0000H ;OGR意思是程序的起始位置,是一个伪指令,不对程序产生作用。在没有学习堆栈的时候,所有程序都是在0000H起始的
JMP START ;JMP是跳转指令,后面写标号,下面的START就是标号,START后面加上一个冒号才能表示一个标号。所以这句就是跳转到START
START:MOV A,#00H
MOV P1,A ;这里手动挪了IO口,把线接在P1口了
RET ;这里用JMP $把程序停在这个地方也是可以的,结果是一样的。RET代表返回
END

二、让LED灯闪烁

用到延时

要学到循环结构和寄存器的使用

下面的程序可以先不用理解,注释也先不看,先看后面的解释之后再来看代码

注意一下,代码中加了冒号的前面内容(绿色的)都是函数名,是可以随便取的,可以取MAIN也可以取START这些都没事,只是自定义的函数名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ORG 00H;程序起始
START:
CLR P0.0;把P0口的第0位清零,灯就亮了
CALL DELAY ;调用DELAY函数
SETB P0.0;SET是置位的意思,B是位,就是说把一个位置位,这里把P0口的第0位,置1。这个和前面的CLR刚好相反一个置0一个置1
CALL DELAY ;调用DELAY函数
JMP START ;跳转到START函数
DELAY:MOV R5,#25 ;写一个延时函数,DELAY就是延时函数的意思。R5是一个寄存器,我们把立即数25移到R5里面去
D1:MOV R6,#100 ;把100放到R6这个寄存器中
D2:MOV R7,#100 ;把100放到R7这个寄存器中
DJNZ R7,$ ;DJNZ指令:减一不为零转移指令。这里的$就是停在这里的意思,和DJNZ R7,DJNZ是一个意思
DJNZ R6,D2
DJNZ R5,D1
RET
END

程序的效果是:

img

最右边的灯是一亮一灭的

这里解释一下DJNZ指令,DJNZ Rn,REL是一条转移指令,先将工作寄存器Rn中的数减”1”判断结果是否为”0”,不为”0”程序就跳转到行标为REL的地方执行,否则,为”0”就不转移,继续执行下一条指令。

需要注意的是每执行一次DJNZ指令时,工作寄存器中的数会先减去1后,再判断是否返回。例如如果Rn中原来是2,则执行两次DJNZ后,Rn中就是0了,DJNZ运行的机器周期为2。

还有一个JNZ指令(或JNE)刚好和DJNZ指令相反,结果不为零(或不相等)则转移

这里还要说明一下指令周期的意思——即CPU执行一条指令所需要的时间称为指令周期。它是以机器周期为单位的

时钟周期——也叫振荡周期,一个时钟周期=晶振的倒数。

51单片机的一个机器周期=6个状态周期=12个时钟周期

51单片机的指令有单字节,双字节和三字节的,他们的指令周期不尽相同,一个单周期指令包含一个机器周期,即12个时钟周期,所以一条单周期指令被执行所占用时间为12*(1/12000000)=1us

这里通过一个c语言程序讲解一下延时程序的原理

1
2
3
4
5
6
Void delay(int z)
{
int x,y;
for(x=z;x>0;x--)
for(y=110,y>0;y--);
}

这个程序是传入一个参数z比如为50,然后x=50开始,y从110一直减到0,然后x=49,y再从110减到0,一共减了50*110次。注意第一个for循环后面没有分号,意思是接下来执行下面的for循环。

这样一个程序就达到了延时的效果,因为这个程序执行完需要一定的时间,如果一次1毫秒,那就是50*110毫秒

接下来我们看一下用汇编写的50ms延时子程序:

1
2
3
4
5
DEL:MOV R7,#200  ;先把200传到R7寄存器
DEL1:MOV R6,#125 ;再把125传到R6寄存器
DEL2:DJNZ R6,DEL2 ;R6不断减1,不为0就继续减,为0后执行下一条语句
DJNZ R7,DEL1 ;R7不断减1,不为0就跳转回DEL1那行,R6又变为125,R7因为减1变成了199;如果为0则结束
RET ;根据循环就是200*125

计算精准延时时间:1 + (1 * 200) +(2 * 125 * 200) + (2 * 200) + 2

=(2 * 125 + 3 ) * 200 + 3

=50603us

=50ms

可以由上面的式子整理出公式即:

延时时间=(2 * 内循环+3) * 外循环 + 3

现在我们来详细说明一下怎么计算延时时间:

​ 第一句:MOV R7,#200 在整个子程序中只被执行一次,且为单周期指令,所以耗时1us

​ 第二句:MOV R6,#125 因为第四句如果为0则会跳回这里执行,那么就数一共跳回了多少次,因为R7是200,所以要减200次R7才能为0,所以跳转了200次,也就是第二句执行了200次

​ 第三句:DJNZ R6,DEL2 本身内循环了R6次,但是又有外循环R7次,所以就是125*200次,注意DJNZ这个指令是双周期指令,所以需要再乘2

​ 第四句:就是R7次,即200次,但是因为DJNZ是双周期指令,所以是2*200

​ 第五句:这个就要2us,固定的

我们再看另一个例子,1秒延时子程序

1
2
3
4
5
6
7
DEL:MOV R7,#10
DEL1:MOV R6,#200
DEL2:MOV R5,#248
DJNZ R5,$
DJNZ R6,DEL2
DJNZ R7,DEL1
RET

延时时间:1 + (1 * 10) + (1 * 200 * 10) + (2 * 248 * 200 * 10) + (2 * 200 * 10) + (2 * 10) + 2

=998033us = 1s

已经很好理解了,需要注意的就是R7,R6,R5的顺序

公式可以归纳为((2 * 最内循环 + 3) * 内循环 + 3) * 外循环 +3

这里我再补充一下51单片机内的寄存器R0-R7

R0-R7在数据存储器里的实际地址是由特殊功能寄存器PSW里的RS1、RS0位决定的

我们只要知道通常情况下R0-R7在数据存储区里的实际地址是00H-07H,如果我们需要08H-0FH的地址,我们是需要对PSW寄存器进行设置才可以使R0-R7在数据存储区里的实际地址变为08H-0FH,这个我们以后再讲,因为不常用

好了全部讲完了,现在再回去看最开始的代码,会发现非常的简单,主函数START先让灯亮,然后调用延时函数,让程序停一下,然后在让灯灭,再调用延时函数,让程序停一下,这样就可以实现灯的闪烁了。

三、流水灯

单片机P1口接八个LED,每次点亮两只,先从右边向左移动点亮,再从左边向右边移动点亮,然后闪烁两次,重复循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ORG 00H
START:MOV R0,#8 ;在R0存入立即数8,这个8就是以后要用到的移动次数
MOV A,#0FEH ;存入第一个灯亮的位置
LOOP:MOV P1,A
CALL DELAY
RL A. ;RL是左移一位指令,RR是右移一位的指令
DJNZ R0,LOOP
JMP START ;跳回START形成大循环
/*下面是上一节一样的延时程序*/
DELAY:MOV R5,#50
D1:MOV R6,#100
D2:MOV R7,#100
DJNZ R7,$
DJNZ R6,D2
DJNZ R5,D1
RET

代码其实已经很简单了,完全看得懂,效果就是灯从右往左依次点亮,不断循环下去。

我们现在把程序变得复杂一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ORG 00H
START:MOV R0,#8 ;在R0存入立即数8,这个8就是以后要用到的移动次数
MOV A,#0FEH ;存入第一个灯亮的位置
LOOP:MOV P1,A
CALL DELAY
RL A. ;RL是左移一位指令
DJNZ R0,LOOP
MOV R1,#3 ;为下一个程序要执行的次数做准备
MOV A,#00H ; 让所有灯全部都亮
LOOP1:MOV P1,A ;执行上一条的,让所有灯点亮
CALL DELAY ;调用延时函数
CPL A ;CPL指对A进行按位取反(0变1,1变0,刚刚所有灯都亮的,现在所有灯都灭了)
DJNZ R1,LOOP1 ;因为R1里面是3,所以执行3次
JMP START ;跳回START形成大循环
/*下面是上一节一样的延时程序*/
DELAY:MOV R5,#50
D1:MOV R6,#100
D2:MOV R7,#100
DJNZ R7,$
DJNZ R6,D2
DJNZ R5,D1
RET

程序的效果是:灯先从右往左依次亮起,然后所有灯亮,灭,亮,灭,再从右到左依次亮起

现在我们讲一下这个程序,因为c语言里面我们可以写带参数的函数,如果要循环几次就传多少的参数,在汇编语言里面不行,那么我们就是在上一句的结尾先用寄存器存好,为接下来调用做准备。

四、数码管(显示数字)

我们看一下数码管的原理

img

一共有八个led灯,比如我们想让它显示1,那么就让b,c亮起来就可以了,如果想要它显示0,那么就a,b,c,d,e,f亮,g,h不亮。这里的h代表小数点

这里讲的是共阳极数码管的编程

下面编写一个程序来驱动一位共阳极数码管从0-F显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ORG 00H
MAIN:MOV DPTR,#TABLE ;TABLE是一个数组,下面会定义 。就是把数组中的值存入DPTR
MOV R0,#0 ;把R0的初始值设为0
LOOP:MOV A,R0 ;把初值给累加器A
MOVC A,@A+DPTR ;间接寻址,取表中的代码,@A等同于指向DPTR的指针。整句话的意思是从DPTR开始偏移A个地址的数读取到A中
MOV P0,A
CALL DELAY
INC R0 ;R0自增1
CJNE R0,#16,LOOP ;CJNE表示如果不相等就转移,这里是如果R0不等于16那么就跳转到LOOP函数继续执行,如果是16了那就接下去执行
JMP MAIN ;这里说明已经从0-F显示了,那么就可以跳回MAIN函数重新大循环一遍,让数码管一直从0-F显示
/*下面是之前写的延时函数*/
DELAY:MOV R5,#50
D1:MOV R6,#100
D2:MOV R7,#100
DJNZ R7,$
DJNZ R6,D2
DJNZ R5,D1
RET
TABLE:DB 0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8 ;这里解释一下DB类似于c语言里面的括号,这里的话数组有几行那么就要在每行的开头写DB
DB ox80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e ;这里写两行代表两个数组
END

现在我们来讲一下DPTR是什么

它是一个16位的寄存器

我们看第五行,@符号是寄存器间接寻址前缀

其实就当指针来理解就好了,现在DPTR里面存了一个TABLE数组,数组里面刚好有16个元素,DPTR也刚好是16位寄存器,然后DPTR的指针地址刚开始是0,0指向0xc0,一直到16,16指向0x8e,A+DPTR,如果A是1,那么加了之后就是1了,那么就指向0xf9,@符号就是取值,所以第五行就是把0xf9这个值存到A里面。

然后我们还要知道这个TABLE数组里面的值其实就是数码管对应亮起的值,比如0xc0就代表数码管亮0

好了,这个程序肯定已经看懂了

五、四位共阳极数码管(显示四个数字)

这里做的效果就是拿一个可以显示四个数字的数码管显示1234

img

看一下这个电路图,从P0一直接到了P13

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
ORG 00H
MAIN:MOV DPTR,#TABLE
LOOP0:
CLR P1.0
MOV A,#1
MOVC A,@A+DPTR
MOV P0,A
CALL DELAY
SETB P1.0
/*下面代码和LOOP0差不多就是第一行第二行和最后一行改了数字*/
LOOP1:
CLR P1.1
MOV A,#2
MOVC A,@A+DPTR
MOV P0,A
CALL DELAY
SETB P1.1
LOOP2:
CLR P1.2
MOV A,#3
MOVC A,@A+DPTR
MOV P0,A
CALL DELAY
SETB P1.2
LOOP3:
CLR P1.3
MOV A,#4
MOVC,A @A+DPTR
MOV P0,A
CALL DELAY
SETB P1.3
JMP MAIN

/*下面是之前写的延时函数*/
DELAY:MOV R5,#50
D1:MOV R6,#100
D2:MOV R7,#100
DJNZ R7,$
DJNZ R6,D2
DJNZ R5,D1
RET
TABLE:DB 0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8 ;这里解释一下DB类似于c语言里面的括号,这里的话数组有几行那么就要在每行的开头写DB
DB ox80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e ;这里写两行代表两个数组
END

这样一个程序的效果就是依次显示1,2,3,4并不是常亮1,2,3,4

这里就要用到动态扫描

最简单的方法就是缩短延时

把第36行的#100改成10之后,就会发现几乎可以常亮,但是还是可以肉眼看到一点闪烁

六、独立按键

独立按键就是输入,就是我们按下按键的时候会有什么样的操作

这里我们把独立按键接到P2口上

1
2
3
4
5
6
MAIN: MOV P2,#0FFH ;P2口初始值都是1,就是都不被按下
/*当P2口有一个按钮被按下,P2口就不会都是11111111了,肯定有一个是0*/
LOOP: MOV A,P2 ;当一个按钮被按下的时候,P2变成比如11011111,我们把这个值移给累加器
MOV P0,A ;再把累加器的值给P0,P0是灯的接口。这是对应的,比如P2的0口被按下,那么P0的0口就会亮
JMP LOOP
END

这个简单的程序就是实现按第几个键第几个灯就会亮

七、外部中断

中断的概念

所谓“中断”,是指CPU执行正常程序时,系统出现特殊请求,CPU暂时中止当前的程序,转去处理更紧急的事件(执行中断服务程序),处理完毕(中断服务完成)后,CPU自动返回原程序的过程

好处:提高CPU效率(实时响应外界信息)、实现并行工作

通俗理解非常简单,比如你正在打王者,这时候微信来了消息,我们就会先把王者挂后台,然后去回微信,这就是执行中断服务程序。然后微信回复完之后又在后台点击王者这个app,然后继续游戏(注意,这时候并没有杀后台,王者不需要重新登陆,直接就可以继续刚刚的游戏),这就是中断服务完成,CPU返回原程序

外部中断:

从外部引脚P3.2或者P3.3引脚接收到的信号称为中断源(比如在看书时,敲门声和电话声就是两种中断源)

外部中断信号由两种类型(敲门门铃,直接手敲门)

电平触发:低电平有效

边缘触发:高电平变为低电平有效

中断申请寄存器IE介绍

八、定时器中断

定时和计数功能最终都是通过计数实现的,若计数的事件源是周期固定的脉冲,则可以实现定时功能,否则只能实现计数功能。因此可以将定时和计数功能由一个部件实现。

实现定时和计数的方法一般有定时、专用硬件电路和可编程定时器/计数器三种方法。

软件定时:执行一个循环程序进行时间延迟。定时准确,不需要外加硬件电路,但增加CPU开销。
专用硬件电路定时:可实现精确的定时和计数,但参数调节不便。
可编程定时器/计数器:不占用CPU时间,能与CPU并行工作,实现精确的定时和计数,又可以通过编程设置其工作方式和其它参数,因此使用方便

51单片机有三个16位的定时器——定时器0、定时器1和定时器2。
定时器0、1各具有四种工作模式;定时器2有两种工作模式
定时器0、1和定时器2的任何一种工作模式均可通过程序对相应寄存器进行设置来选择。

定时器T0和T1都是两个16位加法计数器

51中断入口地址

INTO:0003H

T0:000BH

INT1:0013H

以下程序是用数码管显示的秒表,可以显示到0-9秒(一位共阳极数码管)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
ORG 0000H
LJMP MAIN
ORG 000BH;定时器中断入口
LJMP T0_INT
ORG 00030H
MAIN:MOV DPTR,#TABLE;先把数码管的编码表移到DPTR寄存器中,这是初始化的部分
MOV R0,#0;给R0赋初值0
MOV A,R0;把R0给A
MOVC A,@A+DPTR;然后就是把DPTR的内容移到A
MOV P0,A;再从A移到P0

MOV TM0D,#01H;启用定时器0的工作方式1。即首先对TMOD寄存器初始化
MOV TH0,#HIGH(65535-50000);给定时器0赋初值,HIGH指的是高八位也可以写(65536-50000)/256
MOV TL0,#LOW(65535-50000)
SETB TR0
SETB ET0
SET EA
MOV R1,#20
JMP $

;T0定时器中断的函数
T0_INT:
CLR TR0
MOV TH0,#HIGH(65535-50000)
MOV TL0,#LOW(65535-50000)
SETB TR0
DJNZ R1,T0E
MOV,R1,#20
INC R0
MOV A,R0
MOVC A,@A+DPTR
MOV P0,A
CJNE R0,#10,T0_INT
MOV R0,#0H
MOV A,R0
MOVC A,@A+DPTR
MOV P0,A
JMP T0_INT
TOE:RETI
;下面是数码管的编码表
TABLE:DB 0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8
DB 0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e
END

九、矩阵按键

他和独立按键最大的区别就是独立按键8个io口只能控制8个按键,而矩阵按键就可以用8个io口控制8*8个按键了,大大提高了效率

比如说控制s2就需要1和7