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;结束
|

这就是效果
我们可以改进一下,比如说把二进制改成十六进制
1
| MOV A,#0FEH ;这里需要注意FE代表11111110,H代表十六进制。如果最高位是0~9那么比如9EH可以直接写,但是如果是A~F那么前面必须加一个0才行,即0FEH,FEH错误
|
我们现在想要让高四位的四个灯全部点亮,低四位的四个灯不亮

我们还可以把代码精简一下,不需要用到累加器A,直接把值传到P0口
0F0H就是11110000即低四位的灯亮

我们还有另一种方法点亮灯
CLR是清除位的意思,就是清0寄存器
1 2 3
| CLR P0.0 ;就是说把P0的0口进行清零 JMP $ END
|

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

接下来我们试一试让灯间隔着亮,即一个亮一个不亮
1 2 3
| MOV P0,#55h ;后面的H是小写的h也是可以的 JMP $ END
|

其实上面的程序都不是一个完整的程序,那么什么样的程序是一个完整的程序呢?
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
|
程序的效果是:

最右边的灯是一亮一灭的
这里解释一下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语言里面我们可以写带参数的函数,如果要循环几次就传多少的参数,在汇编语言里面不行,那么我们就是在上一句的结尾先用寄存器存好,为接下来调用做准备。
四、数码管(显示数字)
我们看一下数码管的原理

一共有八个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

看一下这个电路图,从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