微机原理及应用¶
写在前面¶
本笔记基于田翔老师课件,田翔老师2026春夏学期《微机原理及应用》以及8051系列芯片英文手册《MCS-51 Users Manual》整理而来。
二次整理过后如果打算分发(如发布在CC98等),请先告知CC98@skyeye2024,谢谢。该笔记目前只发布在个人网站,严禁未经允许用于任何盈利用途。
硬件部分¶
电脑包含控制器CU,运算器ALU(控制器和运算器并称中央处理单元CPU),存储器Memory,输入Input,输出Output。把这些都在一个芯片上搞出来,就叫单片机
对于51,一字是两字节
存储器按易失性可以分为易失性的和非易失性的,也就是按断电之后数据能否留存来分。除了按易失性分,还可以按访问的特性来分。ROM(read only memory)是只能读不能写的,就是你自己准备好代码给厂家生产出来就不能再改,对于那种已经成熟量产的东西可以用这个,成本低。后面有PROM(program read only memory),就是芯片发到你手上你可以自己写一次,写了这一次之后就不能再改。然后又研发出了EPROM,E是erasable,就是可以进行不止一次的擦除和写入。原始的是用紫外光来擦除,后面研发了EEPROM,多一个E就是electrical,就是电可擦除。而RAM(random access memory)是随机访问存储器,可以访问任意的位置并任意进行读写。SRAM的S是static的意思,就是没有时钟的,异步访问的,51单片机里用的就是这个。SRAM访问速度是最快的,但是占面积也大。常见的内存条是DRAM,D是dynamic,动态。SRAM是你写进去之后只要不断电就一直存着,DRAM是你除了不断电还要时不时刷新一下才能保存。DRAM占面积小。SDRAM的S是synchronize同步的意思,它有时钟。DDR(double data rate)是SDRAM的一种,它既可以在上升沿触发也可以在下降沿触发,所以存储的速率是只能在一个沿触发的两倍。现在来说,通常ROM都是非易失性的,而RAM通常是易失性的
除了RAM和ROM,还有一种叫flash类的,比如12GB+512GB的512GB就是,以及U盘也是,以及固态硬盘SSD,都是flash类的。flash掉电不会丢失,但是访问特性又类似RAM
几位机的概念,一次能处理多少位的数据,就称为几位机。比如现在电脑一般64位,早点的32位。而8051是8位机
对于51,可以简单地把ROM当作程序存储器,把RAM当作数据存储器
总线bus,地址总线AB(address bus)由CPU向存储器寻址,单向。数据总线DB(data bus),涉及读写,双向的。控制总线CB(control bus),对于一个特定的信号来说是单向的,比如读写命令就是从CPU向其他部分的,而发起事件之类的可能是其他部分向CPU的
在51单片机里,地址线有A0到A15,十六位。低八位的地址线在P0,高八位的地址线在P2。这16位能寻址的就是64k个,由于每个地址对应的空间都是八位,也就是一个字节,所以能用的空间也就是64kB

数据总线在P0,8位。P0为什么可以做地址总线也可以做数据总线呢,因为不同的时间内它充当的功能不一样。在这里373是一个锁存器,当P0给出地址信号的时候就记录下来锁存,然后之后P0就输出数据了。锁存用的控制信号是ALE(address latch enable),中文是地址锁存信号。锁存器就是异步触发器,没有时钟,不是边沿触发而是电平触发。
锁存器和寄存器的区别,其实它们都是触发器,用来保存数据的,只是锁存器用来保存的是一个临时数据,后续可能会传其他的,而寄存器保存的是和状态相关的配置信息。
ALE还可以以时钟⅙的频率输出方波,所以它还能用来当时钟。ALE在一个机器周期里会有两次变成高电平又变回低电平,每次它变成高电平的时候向ROM里取一次数据,所以一个机器周期取两次数据,最多取16位
P0口除了做I/O口以外可以做地址总线的低八位,和数据总线的八位(总共八位)

读操作就是CPU传过来一个地址以及读的控制信号,然后地址译码器负责根据地址找到对应的存储单元,然后三台双向缓冲器负责让那个存储单元能连到数据总线,其他的存储单元是高阻态不影响输出,然后就可以把数据给读出来了
片内的地址数据控制总线是隐藏在里面的,进行外部扩展的时候才能说P0和P2是地址总线,然后P0也做数据总线,然后-RD, -WR, ALE之类的这些是控制总线
介绍其他存储器
堆栈是先进后出存储器,FILO(first in last out)。堆栈用SP(stack pointer),stack就是堆栈,SP指向堆栈顶端(也就是最新写入数据的那一层)。堆栈读操作就是退回上一个写数据的地方,不过读操作不会改变原有已经存入的数据。
FIFO(first in first out),先进先出。在这门课用的少
数字后缀B是二进制的,O是八进制,H是十六进制

MCS51总共40个管脚,4×8三十二个I/O口,剩下8个管脚。电源VCC和GND各一个(注意GND也算电源管脚),然后还有时钟XTAL1和XTAL2两个,就剩下四个了。剩下四个就是控制的了。控制里的两个其实已经知道,一个是ALE就是之前提到的控制锁存的那个,然后RESET也是早就熟悉的了。
复位后状态

除了P口、SBUF和SP之外其他都变成全0。P口变成了全1,SBUF状态不能确定,SP变成07H
可以拓展各64kB的RAM和ROM,虽然地址只有16位但是可以用控制信号来切换,有一个-PSEN,是program store enable,是片外ROM读选通
最后还有一个EA是external access enable,就是控制片外ROM的访问允许,标准中文是"内部、外部ROM选择信号"。比如8031没有内部的ROM必须外接,那它的这个管脚就要接地。-EA=1时,CPU执行片内ROM指令,但当PC的值超过4K或8K时,转向片外ROM取指令。当-EA=0时,CPU只执行片外ROM指令。
这是51的汇编语言,MOV是操作码,表示数据传送,语法是MOV [目的操作数] , [源操作数]。此处目的操作数是P0,也就是51的P0 I/O口,而#0FFH是源操作数。#是51的立即数前缀,也就是把0FFH当作直接使用的常数,0和H是16进制数的前缀和后缀(0可加可不加),FF换算成二进制就是11111111。因此这条代码的意思就是让P0为全1
执行上面这两条语句,你无法直接得知A里面是什么值,需要结合P0的外部电路来获得
而如果执行下面这两条语句,可以肯定A里面就是全0
注意用P口做输入的时候,需要先把P口置全高电平,确保接地的三极管截止,否则无法接受输入的信号
STM32和ARM比51更加复杂,对于初学者来说不合适,因此这个课讲51要理解这些基本的东西
数据指针DPTR(data pointer)
对8031,要注意复位的时候外部ROM一定要有0000H这个地址,不然它复位完PC回到0000H没法运行。
很多时候卡bug是因为PC取了错的指令,导致没法运行下去,因此用一下复位回到原来的状态就可以了
P口¶
无论是哪个P口,要从它那里读取东西(输入)之前都必须写FFH,保险起见必须这么做
P是port的缩写

P0你看这里也有VCC啊,为什么说它需要上拉电阻呢?因为这个VCC只有在地址数据复用的时候被启用(也就是只有像MOVX,含DPTR之类的涉及外部扩展的指令才能让它用上),如果P0只是做通用I/O,也就是写MOV A, P0类似这种语句的时候,这个VCC是被disable的,所以做通用输出需要上拉,而做地址数据总线的时候不需要。
三角形是缓冲器buffer,它可以单向地传递信号,和寄存器不同,它不能存数据,只能即时原样输出。它的意义是放大电流,把前后级信号隔开。
左边是一个D触发器,然后右边上面有地址/数据线以及控制线(注意不是“数据控制”)。然后控制线是为了控制那个多路选择器MUX(multiplexer)接上面还是接下面,控制线不和MUX相连,只是控制它接哪一段。
P0口做地址/数据总线
当要输出信号的时候,控制线为高电平,MUX打到上面去和地址数据线那条路连在一起。当地址数据线为高电平,经过与门T0导通(因为控制线也是高电平),而经过非门之后T1截止,因此P0的一个引脚和VCC相连输出高电平。当地址数据线为低电平,经过与门T0截止,经过非门T1导通,因此P0的一个引脚和地相连输出低电平。
当要输入信号到D触发器的时候,控制线为低电平,T0截止。此时MUX打到下面-Q那个位置。注意P0作为地址/数据总线的时候,Q自动变成1,于是-Q=0,于是T1截止。T0和T1都截止了,外来的信号通过下面那条路经过那个三角形写入D,从而完成数据的写入。
P0口做通用I/O口
控制线为低电平,T0截止,MUX打到下面。当要输出高电平的时候,Q=1,-Q=0,于是T1截止,此时P0没法取,因此说要外接一个上拉电阻,上拉电阻就是在引脚和VCC之间有一个电阻,这样就可以输出高电平了。要输出低电平的时候,Q=0,-Q=1,于是T1导通,引脚接地输出低电平。
务必注意,所有P口做通用I/O口,在需要读写状态的场景,凡是要读取信号,都要提前写1,比如MOV P0, #0FFH
现在就可以解释为什么之前的两种代码结果不同了。先执行了写再执行了读的操作,而地址总线是单向的,因此认为这个P0是作为通用I/O口使用。假如是写0,那么写是能写进去,写进去之后Q=0,-Q=1,T1导通,再读的时候能读到低电平(连了地)。但是如果写的是1,那么写了之后Q=1, -Q=0,T1和T0都截止(T0截止是因为控制线低电平),那么再去读的时候,如果没有上拉电阻就是断路的状态,如果有上拉电阻那就能读到1,因此说需要根据外部怎么接来判断
读锁存器确实是能读到存进去的值,但是通常读的时候是直接用引脚而不是用读锁存器,只有一类叫做读-修改-写功能的指令会做读锁存器的操作
ANL是and logic,这个指令对于P0和A按位与然后再把结果写进P0,这个操作读的就不是引脚而是读锁存器
只有当目的操作数是P0而且需要用到它原来的状态的时候才会用读锁存器,像是MOV P0, #0这样就是直接把0写进去不会用到P0原来的值所以就也不会有读锁存器的情况

这些是所有读-修改-写功能的指令,都是读latch里的值而不是读端口上的值,是为了避免误读,因为有些外部连接会导致你写出去的值和你之后读回来的值不一样。
8031不能把P0当作I/O口是因为它必须用外部的ROM,它的P0就只能用来做地址和数据总线了
P1是纯I/O口,比P0简单

读的时候先写1,使得T截止,这样外部输入1的时候进来的是1,外部是0的时候就强制接地也是输入0

P2可以做I/O也可以做地址的,地址是单向的。普通I/O的时候和P1是一样的,虽然接的是Q但是加了个非门,还是相当于从-Q控制T。做地址的时候控制线就使得MUX打到地址那里,那这里就很简单,地址传过来是1那输出就是高电平,是0那T就导通输出低电平,这里不需要把Q改成1,和P0不同

当P2口用作地址总线的时候,比如执行MOVX @DPTR的时候,不会改变P2锁存器里的值,也不需要提前给P2写1。此时P2是临时输出DPH里的值,之后会恢复回原来的P2存的值。无论什么时候执行MOVX @Ri的时候,以P2存的值为高八位,哪怕刚用完MOV @DPTR之后,高八位也不会使用之前的DPH

P3比P2复杂一些,因为它还有其他功能。做I/O的时候那也是输出(写)时Q=1, T截止,Q=0,T导通。输入的话就Q=1,P3经过下面那两个三角形来输入。而第二输出功能就是让T截止(会让Q=1)之后根据P3引脚的值,然后经过一个三角形就取出

用P0做通用I/O的时候要配上拉电阻,但P0做地址数据总线复用的时候不用上拉,它内部的那个上拉会被启用
注意-WR和-RD是通过P3口控制的,用作外部扩展RAM,而外部扩展ROM是用-PSEN控制的
课本上说控制总线12根,把P3口和那四根给算上,如果考试问你就说这样是对的就行

P0口虽然内部有一个拉到VCC的FET管,但是这个管子只有在外部扩展的时候拿它做数据/地址总线复用的时候才会打开,其余时候都是关闭的。如果你想用P0做普通的I/O,必须接上拉电阻。对P0这一位写1,它内部的两个FET都是截止的,对外相当于悬空,如果你不外接上拉电阻,那么电位是不确定的。对它这一位写0,下面拉到地的FET管打开,此时无法接收输入,外面无论输入0还是1得到的都只是0。因此操作P0的时候,如果不是外部扩展,如果只是读,那你就需要先写1;如果你想写,需要外接上拉电阻才能满足写0是0写1是1。P0被称为真双向口,真双向的意思是“输入高阻、输出开漏(开漏你简单理解为OC门那种就行)”。而P1, P2, P3被称为准双向口,因为它们默认情况下不是高阻浮空。P1, P2, P3内置上拉电阻,做输出的时候写0就是0写1就是1;做输入的时候需要写1,使得接到地的FET截止,否则读不进来。
而P0做数据地址总线复用的时候,不需要外接上拉电阻。

外部扩展的时候不要往P0里写东西,而且如果P0被用于外部扩展,那么它里面原本存的东西会被抹掉
关于P口讲了很多东西,但是实际上很多东西是用不到的,我这里就把最重要的给记录下来,也就是写代码和接线的时候要注意的
软件部分,需要用P口做普通I/O的读的时候,读之前都必须“1”,无一例外。其他时候,比如说写,或者做ANL之类的,不需要做额外操作
硬件部分,P0做普通I/O的时候必须上拉电阻,做地址数据线的时候不用上拉。其他的P1, P2, P3不用上拉
简答题:
为什么说P0口是三态双向口而P1, P2, P3是准三态双向口?
当向P1、P2或P3口的锁存器写“1”时,下拉场效应管截止,但内部的上拉电阻依然连接在电源和引脚之间。因此,此时引脚是被内部上拉电阻“拉”到了高电平状态,不存在悬空的高阻态。对P0写“1”时,上拉、下拉场效应管都截止,在没有外部上拉电阻时呈现高阻态。
ROM和RAM¶
程序存储器ROM用来存程序以及常数比如#0FFH这样的,以及一些表比如平方表或者乘法表,因为对它来说乘除法计算是最花时间的,如果提前算好可以减少运算时间。
对于自带了ROM的,就算外部扩展ROM,也最多只能64kB,因为它的地址都是用那些线,已经定死了上限了,不是内部的4k/8k+外部64k这么算。

注意51内部ROM是4k,52是8k

对于RAM,和ROM不一样,RAM可访问的最多是内部的256B加上外部的64kB,它是可以直接加的,片内RAM用MOV操作,片外RAM用MOVX操作,X是expand,对于ROM(无论片内片外)都用MOVC操作

对于52严格来说内部并不是256B而是3×128=384B,不过考试你还是就写256B就行

想要访问SFR就直接寻址,直接用80H这种,如果是想要访问高128B的RAM,只能用间接寻址@,比如@R0,R0存放80H。不要间接寻址51的高128B,可能会出错或者返回一个没有意义的值

我才知道原来R0到R7有四组,并不是只有一个。00H是PSW.4=PSW.3=0时的R0所占的那一个字节,01H是PSW.4=PSW.3=0时的R1所占的那一个字节,08H是PSW.4=0, PSW.3=1时的R0所占的那个字节。我一直以为R0到R7各自只有一个,原来有四个。至于要用哪一组你自己分配,一种可选的方式是PSW.4=PSW.3=0时的R0到R7是main程序用的,而PSW.4=0,PSW.3=1时的R0到R7是低级中断的时候用的,10时的是高级中断的时候用的,11时的是备用
但是遇到中断它不会自动切换,需要你手动修改PSW才会用其他组。所以你如果在中断程序里不修改PSW需要对主程序用到的,然后中断程序想用的R做保护。
展开看位寻址区

注意,直接寻址和位寻址的20H是不一样的,直接寻址比如说MOV A, 20H,找的是图中位寻址区07到00这八位。而MOV C, 20H,找的是24H那八位中标号为20的那个bit
SP在复位后会自动跑到07H,但这是01对应的R0到R7,不太推荐(因为那是1区的位置,如果你要用其他区的R0到R7那就把SP放到上面去),一般会推荐放到数据缓冲区,就是位寻址区上面的部分,一般来说调到60H即可
演示一下位寻址的写法
SFR¶

只有最左边那一列的是可以位寻址的,像是TMOD,PCON就无法被位寻址,需要整个赋值。T2CON, TH2, TL2, RCAP2L, RCAP2H只在8052里面有,这门课不要求。
注意这里面没有PC,它不是特殊功能寄存器
介绍一些特殊功能寄存器SFR(special function register)
用到ALU的基本上都要用到累加器A,而B寄存器通常是进行乘除法操作的时候搭配A来用,做乘法的时候B存放高8位,A存放低8位,做除法的时候商放在A,余数放在B
程序状态字PSW(program status word),用来存放累加器A的状态以及计算结果的状态,
| D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
|---|---|---|---|---|---|---|---|
| CY | AC | F0 | RS1 | RS0 | OV | - | P |
C是PSW.7,P是PSW.0
CY是carry,进位,进行八位加法的时候,如果要进到第九位,那CY=1,进行八位减法的时候如果是小的数减大的数也会使得CY=1
AC是半进位标志,也就是八位里的后四位运算的时候需要进到第五位那就会AC=1
F0是flag,供用户自己定义它的数值,可以用来做条件判断
RS1和RS0是用来区分RAM里面那四个区的,当RS1=RS0=0,则选中第0区的R0到R7,对应的地址从00H到07H,就是R0对应00H,R7对应07H,后面的也类似
OV是溢出标志,就是有符号数进行运算的时候看有没有溢出,有溢出OV=1。事实上8051很少用有符号数,后面基本见不到OV。我查了一下,MUL AB的结果如果超过255那么OV会变成1,否则为0。然后DIV AB如果B=0的时候OV也会变成1
不要用D1位
P的规则是,ACC里的1的个数是奇数那么P=1。把它称为“奇校验位”,被校验的是A里1的个数是否为奇数。它和数据传输时的奇校验的不太一样,数据传输时的规则是,确保含校验位的所有位的1的个数是奇数,偶校验的规则是,确保含校验位的所有位的1的个数是偶数。
堆栈指针SP也是特殊功能寄存器里的,用来存放栈顶地址
SP可以改到60H,因为复位之后SP默认是07H,但是这样会占用一区的R0到R7,如果多了还会波及上面的,所以改到数据缓冲区,也就是30H以上会更好。堆栈操作的时候用PUSH写入数据,PUSH ACC分两步,SP+1,然后把ACC里的值放到SP增加后指向的位置。用POP读数据也是分两步,读出SP指向位置的数据之后SP-1
旧版的田翔老师课件上会出现PUSH A, POP A。但是如果严谨的话应当是PUSH ACC和POP ACC,因为用A和ACC对accumulator的寻址方式是不一样的。之前之所以那样写,是因为很多编译器它知道你写PUSH A就是想要PUSH ACC,所以那样写也能跑,但是如果从严谨的角度来说还是应当在堆栈操作里都用ACC。A和ACC是不一样的,ACC代表的是直接地址,而A虽然也是指ACC,但是A会用寄存器寻址,这得在后面寻址方式部分讲。反正PUSH和POP置零只能用直接寻址方式,于是只能用ACC不能用A
注意使用LCALL会让堆栈存入下一步要执行的地址,而且是压两个字节
16位的特殊功能寄存器只有DPTR, T0, T1,注意PC虽然是16位的但不是特殊功能寄存器
DPTR也可以拆开两个八位来用,DPH和DPL。可以直接对DPTR赋值,但是不能直接对PC赋值,不可以MOV PC [某值]
时序¶

51的主频最高是12M,由此可以得到一个机器周期最短是一个微秒,最简单的指令也得要一个机器周期,四个机器周期的指令只有乘除法,没有三个机器周期的指令。很多manual里一般就把1微秒作为周期的常数了。
待机是通过停止时钟来完成的,大部分功耗都是开关导致的动态功耗,而静态功耗是很低的

一个机器周期分为六个S,每个S里又有P1和P2(P1和P2各是一个晶振周期)。在一个机器周期里会对ROM读两次,是为了读那个指令。就算只需要读一次它也会读两次,第二次会被忽略
所以三字节指令一定是两周期。因为已知指令只有1周期,2周期,4周期。而4周期指令只有MUL AB和DIV AB,这两个是单字节的。而一个周期只能从ROM取两个字节,那三字节指令肯定取不完,所以需要两个周期。

INC DPTR是2周期的但是只需要从ROM里取一次数,所以之后的3次通过PC从ROM里取的值都会丢掉
MOVX那个指令,因为需要从外部拿数据所以需要更长的时间。后面会知道MOVX指令只有四句,每个都是1字节,2周期的,比较好记。这里时序图上第一个周期S4的时候ALE取了数但丢掉了,但是它更加重要的作用是给P0输出的DPL锁存,然后接下去第二个周期的S1的时候ALE不会变成高电平,也不取指,而第二个周期的S4,ALE虽然会变成高电平但是也不会取值,具体的可以看下面的关于MOVX的时序图,第一张是MOVX从外部RAM读的时序,第二张是MOVX从外部RAM写的时序,可以看到此时ALE两次高电平隔了一个周期,而不是像之前那样保持一个周期里面有两个


软件部分¶
所有硬件能支撑的指令的集合称为指令集,通常硬件越复杂,指令集越大,那么你软件方面就越简单,而如果硬件简单,那么指令集就小,你要执行相同难度的任务你要写的软件代码就越麻烦
计算机只认机器码,因为难背所以用了助记符,助记符和特定功能的机器码一一对应
汇编语言的格式
[标号: ] 操作码 [目的操作数], [源操作数]; [注释]
标号可不写,注释写在;后面
标号的作用是相当于另外起了名字,比如LCALL ASCII,其实是有ASCII: ADD A, #30H,那ASCII相当于是这段代码的别名
从功能上分指令,有数据传送类,数据处理类(运算),控制转移类
控制转移类指令就是像是循环那种,它可以隐性改变PC值

我来解释这个程序。这里是被写好的ROM,ROM里的这些地址里包含的数据都是写好的了,可以看到从3000H到3007H是程序,而3008H到3017H是ASCII码表,程序和ASCII码表连在一起。注意这个图不包含R0和A的具体值,机器码、十六进制码和指令助记符描述的是相同的东西,都是指令。
当你要用这个程序的时候,PC跳转到3000H开始取指,把R0放到A去,然后保留A的后四位,然后给A加2,然后把A的值当作地址,加上此时PC的值(即3006H,下一个要执行的指令的地址),从ROM的这个地址取出数,放到A里(MOVC就是从ROM里取)。这一步实际上发生了什么呢,我们用具体的R0来看。假设R0是00000000,那么经过前面几步得到了00000010存放在A,那么这一步就是用00000010+3006H,也就是在ROM里往下两个地址,来到3008H。3008H这个地址存放的是30H,也就是0对应的ASCII码,那么找到这个数据就存到A里面去。最后一个RET就是return的缩写,这部分程序结束。
从这里也可以看出指令占的字节,MOV A, R0就相当于E8H,是一个字节的指令,而ANL A, #0FH相当于54H和0FH,是两个字节的指令。简单记忆就是任何指令至少要有一个字节,如果要在A以外另外找一个立即数,存在ROM里的,那么需要多一个字节,如果需要找两个立即数,就要多两个字节。一个指令最多占三个字节。
为什么MOV A, R0只需要一个字节呢,明明它也要从R0里取数据啊。原因是操作码里的后四位可以表达R0到R7,比如MOV A, R0对应的机器码是11101000,这里1000就是对应R0,如果后四位是1111就是对应R7
可能会出现16位立即数,只有一种用法就是给DPTR赋值,因为地址是16位的


寻址方式¶
==立即寻址==是从ROM里寻址的方式,比如立即数就是这样,立即寻址的代码至少两字节,因为它要从ROM里找数
MOV DPTR #8000H是三个字节的,第一个字节是告诉你要往DPTR里要放一个数这个事情,然后这个立即数作为地址是十六位的,需要两个字节来表示,所以总共是三个字节。另外这个代码是双周期的,要知道代码只有1或2或4个机器周期的,4周期的只有乘除法,这个不是乘除法所以排除。而一个周期处理不完16字节的,所以就需要两个周期
==直接寻址==是在片内RAM或SFR里寻址的方式,操作数不用加#。直接寻址是访问大多数SFR的唯一方式(除了A, B, DPTR这些通过寄存器寻址也能直接寻址的寄存器)

光写A就是寄存器寻址,写成ACC其实用了直接寻址,需要多一个字节存地址。而B只有在乘除法指令里用寄存器寻址,如果是MOV A, B还是对B用了直接寻址,这是2字节的指令。
MOV A, 30H; 是把30H这个地址存放的数找到然后放到A里面,是两个字节的指令,单周期。
ANL 30H, #30H; 是把30H这个地址存放的数找到,和立即数30H按位与,得到的结果存放在30H这个地址,这是三个字节的指令,因为要找两个数。双周期,在一个机器周期内最多取两次数,不可能是单周期,而又不是乘除法,所以是双周期。三字节的指令一定是双周期
MOV 30H, 50H; 是把50H这个地址的数放到30H这个地址里面去,是三个字节的指令,因为每个地址都要用一个字节
==寄存器寻址==是从R0到R7或A, B, DPTR寻址的方式,比如MOV A, R3; ,在manual里把访问R0到R7的指令称为register instructions, 把访问A, B, DPTR的指令称为register-specific instructions,老师上课的时候貌似把这两种都称为了寄存器寻址。
寄存器间接寻址(有时也简称间接寻址)是把寄存器里的数据作为地址来寻找这个地址存放的数,比如@R0,就是把R0存的数作为地址,寻找这个地址对应的数。用这种方法寻址片内或片外的RAM,52里高128B的RAM也要靠间接寻址来访问。
想要把数据存入52的高八位RAM,不能用MOV 80H, #0FFH,而是要用
MOV 80H, #0FFH会把数据存入SFR的80H(P0),没法存进高八位RAM里,只有用@R0这种方式才能访问52的高八位RAM
用@DPTR访问的一定是外部存储器,访问方式比如说MOVX A, @DPTR; 这里需要DPTR里已经写好了要访问的地址
MOV @R0, A是单字节,MOVX A, @DPTR也是单字节。这里我要重新说明这个字节数是存在ROM里的指令的长度,什么情况下需要更多的字节呢,就是比如说直接存了八进制数在程序里面,或者直接存了地址在程序里面才需要,但是这里只是涉及到取用的操作,不需要你把DPTR的具体内容存到程序里面,所以是单字节的。

你看这样才是需要双字节的,因为它把这个数直接存到程序里了。除了存数,还可以存地址,比如30H也是把这个地址写到里面去了,但是你@DPTR只是告诉它要做一个从这个寄存器取数的操作,不用另外告诉它这个寄存器的地址
基址寄存器加变址寄存器间接寻址,寻址空间是ROM,只有@A+DPTR或@A+PC这两种形式,这种用法就是用来查表,就是常数已经存到ROM里了你就用这种方式去查
这个寻址方式的操作码只有MOVC,不会有其他的MOV,也只有这种方式DPTR的值还是用来访问ROM的
==相对寻址==用到rel,也是在ROM里寻址的。相对寻址不是为了拿一个数而是为了改变PC的值,用法比如说
JNZ 标号(rel); 其中JNZ是Jump if Not Zero的简写(这里要求不为零是指累加器A不为0),然后rel就是你想要的偏移量。这条代码本质上是把当前PC的值和rel的值做一个加法再赋值给PC

rel是多少它自己会帮你算好,跳转指令你写标号就行。
CLR C把进位标志位C清零,是1字节的,没有操作数
SUBB A, #20H把A和立即数20H相减(带借位,不过由于上一条CLR C已经把借位C置零,所以这里相当于还是只是A-20H),结果存放在A,如果A-20H>0则不发生借位,C=0,如果A-20H<0则C=1。这条代码两字节,因为除了操作之外还有一个立即数
JNC LOOP1,其中JNC是jump if not C,也就是如果C=0则跳转到LOOP1,对应A>20H的情况。这条代码两字节,因为除了操作之外还把偏移量rel写入了ROM占第二个字节,rel的计算都是用目的地址-源地址-2,为什么呢,因为凡是这种跳转的指令都是2字节,所以PC取了这条指令之后就会+2指向下一条,这时我们要使得PC移动到LOOP1,因为PC已经向下移动了2位,所以只需要再移动目的地址-源地址-2就行。

rel取值范围-128到+127,Byte
如果符合C=0会跳转到LOOP1,如果C=1就会继续执行下一个指令MOV A, #10H。执行完之后SJMP是short jump,就是无条件跳转到LOOP2,也是2字节指令

注意这里是十六进制,10H对应16,所以并不会发生30H和40H之间有重叠的情况
ORG 2000H是伪代码,只是告诉你代码从2000H开始,不是你会用到的汇编语言
以上程序中的LOOP相当于
其中R2相当于i,是一个计数器,要求执行十六次。这里我刚开始写的时候还搞错了,写成了i>1,因为我是对着DJNZ写的,DJNZ是decrement and jump if not zero,也就是减一之后如果不是0就跳转,所以我就想着是i>1才对,但是区别在于for循环是一开始做判断,但是这里是最后才做判断,相当于do while了,所以它是要多一次的,如果是i>1那其实循环才执行十五次
另外如果R2=00H,遇到了DJNZ R2,那经过一次之后R2会变成FFH,所以总共要把循环执行FF+1=256次
那么再从头解释一下汇编代码的意思,就是先让R2作为计数器存了16,然后让R0做第一个指针a,R1做第二个指针b。循环部分就是把R0地址对应的值先放到A再从A搬到R1,不能MOV @R1, @R0的原因是它不支持这个功能。
然后INC是increment的前三个字母,表示加1的功能,就是把R0和R1各自往前移了一位,相当于c代码里的a++和b++。然后DJNZ是对R2-1,如果结果不是0就跳回LOOP。注意JNZ看的是A是否为0,但是DJNZ看的是放在逗号左边的那个-1是否为0
注意只有R0和R1能做寄存器的间接寻址,所以这里用的是R0和R1,后面的不能用
对于这种往前跳的偏移量,公式是一样的,不过现在要用补码的形式表示,这应该是少有的要用到补码的地方

补码是反码加1,FFH(16进制的全1)减去那个就是反码了,后面还加了1
RAM内部还有一块位寻址区,就是用来做==位寻址==的,除了那一块之外,还有SFR的这些也是可以位寻址的

位寻址和直接寻址的样子一样,比如说10H等等,那如何区分的,就靠指令来区分
可位寻址的片内RAM分布在低128位的位寻址区(20H-2FH)和高128位的SFR中可被8整除的地址,比如80H,88H等
CLR 10H; CLR的这种用法就是位寻址,是把10H这一位给清零
MOV 10H, #00H; 这是通常的MOV的用法,这是对10H这个字节进行的操作,不是位寻址
MOV 10H, C; 凡是看到C就知道这是SFR里的那个进位标志位,它是只有一位的,凡是在MOV里面看到C,就知道这是位寻址,是把C的这一个bit给写到10H这一位上
SETB 92H; SETB是set bit的缩写,一看到这个就知道是位寻址,是把92H这一位给置1
注意MOV P0, #0FFH是三字节的,第一个字节是要MOV这件事,第二个字节存P0的地址,第三个字节存立即数。P0是直接寻址的,它是一个地址,不是默认知道的。R0到R7是不需要多一个字节来存的。P0到P3也是特殊功能寄存器
凡是位寻址,都是XXH.X这样的形式,只是一些关键词可以用来替换XXH或者替换XXH.X,比如P0.1就是用P0=80H替换了XXH,而SETB ES是把整个XXH.X都替换了,这两种都是位寻址
替换字节,就是对于它来说输入这个等于输入了XXH,比如输入P0它会直接替换为80H
| 字节别名 | 对应字节地址 | 含义 | 位寻址写法例子 | 备注 |
|---|---|---|---|---|
| P0 | 80H | P0 口寄存器 | SETB P0.0、CLR P0.7 |
所有 IO 口均可位寻址 |
| P1 | 90H | P1 口寄存器 | MOV C, P1.5 |
|
| P2 | 0A0H | P2 口寄存器 | ANL C, P2.3 |
|
| P3 | 0B0H | P3 口寄存器 | ORL C, P3.2 |
P3 口各位还有独立位别名(见第二类) |
| PSW | 0D0H | 程序状态字寄存器 | SETB PSW.1、CLR PSW.7 |
PSW.7 就是 C(进位位) |
| ACC | 0E0H | 累加器 | CPL ACC.0、MOV C, ACC.7 |
注意区分 A(字节操作)和 ACC.x(位操作) |
| B | 0F0H | B 寄存器 | SETB B.4 |
乘法 / 除法专用,可位寻址 |
| IE | 0A8H | 中断允许寄存器 | SETB IE.7(等价 SETB EA) |
各位有独立位别名(见第二类) |
| IP | 0B8H | 中断优先级寄存器 | CLR IP.2 |
各位有独立位别名 |
| TCON | 88H | 定时器 / 计数器控制寄存器 | SETB TCON.0 |
各位有独立位别名 |
| SCON | 98H | 串口控制寄存器 | CLR SCON.1 |
各位有独立位别名 |
| TMOD | 89H | 定时器 / 计数器模式寄存器 | 无位寻址(仅字节操作) | 虽为 SFR,但不可位寻址 |
| SBUF | 99H | 串口数据缓冲寄存器 | 无位寻址 | 不可位寻址 |
| PCON | 87H | 电源控制寄存器 | 无位寻址 | 不可位寻址 |
| 位别名 | 对应位地址 | 等价写法(SFR. 位序号) | 功能含义 | 典型位寻址指令示例 |
|---|---|---|---|---|
| EA | 0A8H.7 | IE.7 | 总中断允许位(1 = 开启所有中断,0 = 关闭所有中断) | SETB EA / CLR EA |
| EX0 | 0A8H.0 | IE.0 | 外部中断 0(INT0/P3.2)允许位(1 = 开启,0 = 关闭) | CLR EX0 / SETB EX0 |
| ET0 | 0A8H.1 | IE.1 | 定时器 0(T0)中断允许位(1 = 开启,0 = 关闭) | MOV C, ET0 / SETB ET0 |
| EX1 | 0A8H.2 | IE.2 | 外部中断 1(INT1/P3.3)允许位(1 = 开启,0 = 关闭) | SETB EX1 / CLR EX1 |
| ET1 | 0A8H.3 | IE.3 | 定时器 1(T1)中断允许位(1 = 开启,0 = 关闭) | CLR ET1 / MOV C, ET1 |
| ES | 0A8H.4 | IE.4 | 串口中断允许位(1 = 开启,0 = 关闭) | ANL C, ES / SETB ES |
| PX0 | 0B8H.0 | IP.0 | 外部中断 0(INT0)优先级控制位(1 = 高优先级,0 = 低优先级) | SETB PX0 / CLR PX0 |
| PT0 | 0B8H.1 | IP.1 | 定时器 0(T0)优先级控制位(1 = 高优先级,0 = 低优先级) | CLR PT0 / MOV C, PT0 |
| PX1 | 0B8H.2 | IP.2 | 外部中断 1(INT1)优先级控制位(1 = 高优先级,0 = 低优先级) | MOV C, PX1 / SETB PX1 |
| PT1 | 0B8H.3 | IP.3 | 定时器 1(T1)优先级控制位(1 = 高优先级,0 = 低优先级) | SETB PT1 / CLR PT1 |
| PS | 0B8H.4 | IP.4 | 串口中断优先级控制位(1 = 高优先级,0 = 低优先级) | CLR PS / SETB PS |
| TF0 | 88H.5 | TCON.5 | 定时器 0(T0)溢出标志位(1 = 溢出,需软件清零) | MOV C, TF0 / CLR TF0 |
| TR0 | 88H.4 | TCON.4 | 定时器 0(T0)运行控制位(1 = 启动,0 = 停止) | SETB TR0 / CLR TR0 |
| TF1 | 88H.7 | TCON.7 | 定时器 1(T1)溢出标志位(1 = 溢出,需软件清零) | CLR TF1 / MOV C, TF1 |
| TR1 | 88H.6 | TCON.6 | 定时器 1(T1)运行控制位(1 = 启动,0 = 停止) | SETB TR1 / CLR TR1 |
| IT0 | 88H.0 | TCON.0 | 外部中断 0(INT0)触发方式位(0 = 低电平触发,1 = 下降沿触发) | CLR IT0 / SETB IT0 |
| IT1 | 88H.2 | TCON.2 | 外部中断 1(INT1)触发方式位(0 = 低电平触发,1 = 下降沿触发) | SETB IT1 / CLR IT1 |
| RI | 98H.0 | SCON.0 | 串口接收完成中断标志位(1 = 接收完成,需软件清零) | CLR RI / MOV C, RI |
| TI | 98H.1 | SCON.1 | 串口发送完成中断标志位(1 = 发送完成,需软件清零) | SETB TI / CLR TI |
| REN | 98H.4 | SCON.4 | 串口接收允许位(1 = 开启串口接收,0 = 关闭) | SETB REN / CLR REN |
| C | 0D0H.7 | PSW.7 | 进位标志位 | SETB C / CLR C |
| AC | 0D0H.6 | PSW.6 | 辅助进位标志位(BCD 码运算进位 / 借位判断) | CLR AC / MOV C, AC |
| F0 | 0D0H.5 | PSW.5 | 用户自定义标志位(可自由赋值,用于程序状态判断) | MOV C, F0 / SETB F0 |
| RS1 | 0D0H.4 | PSW.4 | 工作寄存器组选择位 1(与 RS0 配合选择 4 组工作寄存器) | SETB RS1 / CLR RS1 |
| RS0 | 0D0H.3 | PSW.3 | 工作寄存器组选择位 0(与 RS1 配合选择 4 组工作寄存器) | CLR RS0 / MOV C, RS0 |
寄存器寻址不需要多一个字节来存放地址,会包含在第一个字节里面
51的ROM和片外RAM不能直接传输,所以不能直接把立即数传给片外RAM,需要先传到片内RAM或SFR。因此立即数永远都是用MOV这条代码,只在片内进行移动。如果要穿给片外RAM需要用MOVX

别忘了位寻址
这个图里的CY并不会在代码里出现,代码里只有C,所以含C的,这个C用到的是寄存器寻址而不是位寻址?C是位寻址吗?
寄存器寻址和寄存器间接寻址都是不需要多一个字节的
1个机器周期会在ROM里取两个字节,所以双字节单周期是可以的
三字节指令至少要两个周期,非乘除法的一定是两个周期

外部扩展RAM无法直接寻址,需要配置Ri和P2来分别制定低八位和高八位
任何型号,无论31,51,52,都是有片内RAM的,所以没有8031用不了A,DPTR之类的问题

这里的register instructions和register-specific instructions在中文部分统称为寄存器寻址,不加区分

这里的INDEXED ADDRESSING在中文介绍里区分成了基址寄存器加变址寄存器寻址和相对寻址
指令相关¶
不允许MOV R0, R1, MOV R0, @R1等
MOV P1, P3是可以的,这两个都是直接寻址
注意MOV Rn,direct是两个周期的,MOV Rn, #data是一个周期的
比较特殊的是MOV DPTR, #data16,用十六位立即数对DPTR赋值,是三字节指令,两周期

MOVX A, @DPTR会产生-RD作为控制信号,MOVX @DPTR, A会产生-WR作为控制信号,注意区分开方向,不要写错了
@Ri只能外部寻址八位的地址,但高八位怎么办呢,注意高八位看的是P2的值,和DPH没有任何关系
MOVX指令都是两个周期的,因为P0口复用需要花更多的时间
只有查表的时候需要用到MOVC,当搞了外部扩展ROM的时候,-PSEN会用来做控制总线
MOVC和MOVX的指令里必须含A,不能有其他的。MOVC是双字节,双周期的。MOVX是单字节双周期的。MOVX是双周期是因为它要用到P0做地址和数据的复用。遇到MOVX的时候,ALE就不会以六分之一时钟周期输出了,在P0作为数据线的时候ALE就为0了
如果忘记了,那最不会出错的方法就是先传到A再传到你想传的地方,因为A和任何地方都是连接的
用R0和R1间接寻址片外RAM只能访问低八位的,要访问64K的必须用DPTR
只有用MOVX的时候P3的-WR和-RD才会作为控制总线,其余时候可以用作I/O口
PUSH和POP都接direct直接地址,direct包含R0到R7,各种H的。PUSH和POP要成对出现
注意PUSH只能用PUSH ACC(这是个双字节指令),不能用PUSH A,因为PUSH和POP都只用直接寻址,没有寄存器寻址。A和ACC的差别在于,出现A就是对A寄存器寻址,出现ACC是对同一块地方做直接寻址。通常情况下,用A而不是ACC可以省ROM,但是在PUSH和POP操作里必须用ACC。除此以外,SETB和CLR也必须用ACC.0这样,不能用A.0
然后PUSH和POP是双周期的,因为它要做的事情比较多,单周期搞不完
对于POP SP有更特殊的规定

所以POP SP的功能就是把SP当前指向的内存里放的值赋给SP,之后不再-1
这条指令的意义在于如果有不同的堆栈那可以通过这种方式切换,但是注意这是非常规的,非常少用,只要知道它执行之后发生的事即可

XCH是exchange的缩写,XCHD是exchange digit的缩写,XCH A, Rn是单周期的

DPH和DPL分开存进去,注意POP的时候和PUSH的时候对称的,反着来的,先进后出。
在改变A的数据时,注意P,就是PSW里的那个奇偶校验位也会变化
进行算术运算的时候用到ALU,结果一定放在A里面,一定是ADD A, 某某的形式

注意CY和OV各论各的。CY是当成八位无符号数来看,如果有进位就为1,而OV是当成有符号数来看,如果两个正数相加得到负数,比如例二的情况,那它就变成1了,而例一是两个负数相加得到正数,所以OV也是1。
AC是半进位,P是奇偶校验位
ADDC是带进位的加法,应用场景就是低八位加完可能有进位,那再进行高八位加的时候就要加上这个进位的1
减法只有一种形式,没有不带借位的减法,所以你如果要做减法然后不确定借位有没有的话就必须先做一个CLR C


务必注意MUL AB和DIV AB运算后A和B分别放的是什么
MOV direct, A; 含A的是可以单周期完成的,但是不含A的MOV direct基本是双周期的
INC A只影响A的值和奇偶校验位的值,不会影响CY和OV
自减指令是DEC,像是DEC A这样。不过相比自增指令少了一条,没有DEC DPTR,这个务必注意
想要实现DPTR-1,就得自己写

先把C清零,然后对低八位减一放回去。然后注意还要对高八位做一次SUBB,因为对低八位做减法的时候可能会使得C=1,那你就要再对高八位做带借位减法,然后再把高八位放回去。对所有十六进制数的加减都得这么搞
注意ADD和SUBB只能以A为目的操作数
想要把加法结果用十进制表示的话,就在加法之后加一个DA A的指令就行。这个调整只对加法有效,对减乘除不行

DA A得到的还是BCD码


就是低位和低位加,高位和高位加,中间需要调整到十进制再放回去。这里为什么不用MOV A, 30H; ADD A, 32H呢,是因为用R0和R1这两个指针更加容易拓展,下次如果要改,只需要改R0和R1的值就行,不用把所有出现30H和32H的地方都改掉,而且如果你直接用30H的话也不能用INC来指向下一个了,还得自己写31H和33H
注意这里有处理进位。其实最开始应该有一个CLR C的。2字节压缩BCD码有16位,根据4位表示一个数字,可以表示4位十进制数,比如1234可以拆成0001 0010 0011 0100这样存在两个字节里面。这里怎么处理进位呢?在半字节内部发生的进位通过DA A给映射到了十进制里面,所以这个不用我们自己写。而表达低两位的(34)和加数相加之后的进位存到了C,在后续ADDC中有体现。

这里也是一样的,这就体现出用指针的好处了,就可以写循环了。第一次循环的时候ADDC确实没用,但是不能换成ADD,因为后续是可能有C的。
RL A; 是循环左移,这里L是left的意思,比如10000000变成00000001,就是最高位跑到最低位去了。循环右移RR A; 也一样的道理。
RLC A; 是带C的循环左移,相当于给8位多一位变成了一个9位数,左移的时候C的值跑到最后面,而8位数的最高位跑到C。比如C=1,A=01100011,那么RLC A; 之后就会变成C=0,A=11000111
RRC A; 是带C的循环右移,也是相当于多一位,不过是C跑到最前面,然后8位数的最低位跑到C,比如C=1,A=11000110,经过RRC A;之后变成C=0,A=11100011

CPL A; 是对A取反,是converse position logic的缩写
注意只有CLR A这一条指令是对整个字节清零的CLR指令,其余CLR都是对位操作。类似的,也只有CPL A这一条是对整个字节按位取反,其余的都是对位取反。
ANL是and logic,ORL是or logic,XRL是exclusive or logic异或
这些指令都可以按这样写

后面两个数字,第一个数是字节数,第二个数是周期数
ANL用来保留和屏蔽字节中的某些位,想要保留的就和1与,想要屏蔽的就和0与
ORL用来保留和置1,想要保留的就和0或,想要置1的就和1或
XRL用来保留或取反,想要保留的用0异或,想要取反的用1异或

LJMP是long jump,接一个十六位地址,相当于直接给PC赋值了,这个指令三字节,因为16位地址需要两个字节装。
AJMP是absolute jump,接一个很特殊的十一位地址,它只需要两个字节。十一位地址有8位装在多一个字节,还有3位装在第一个字节的高3位,这样减少了空间。这里特别强调了它会导致PC+2,而且这11位是赋给PC的低11位。PC+2是因为这个指令占两个字节,执行的时候PC已经跳到下一个指令的地方了。如果PC+2造成了PC的第12位改变,那么跳转的地方就可能不是像原先设计的那样了。比如说,执行这条语句前PC=0000011111111110,执行的时候PC变成了0000100000000000,那么这时改后11位,前五位就不是00000而是00001了,这一点要注意
SJMP是short jump,可以在PC所在的位置进行-128到127的跳转
JMP就是jump,就是你往A里放一个数,那么就在DPTR原来指向的位置的基础上偏移这么多。
接下来是有条件的跳转

CJNE用来做条件判断的,它还会影响C,可以用这个作为条件分支的标志。这个是三字节的,这件事一个字节,direct一个字节,rel一个字节,三字节就肯定是2周期的

LCALL是long call,三个字节,因为存十六位地址需要两个字节。然后呢它做的事情就是把地址存进堆栈然后跳转,然后遇到RET的时候再从堆栈把地址取回来然后就可以返回了。ACALL和AJMP类似,也是用两个字节就完成了。RETI是return interupt,做的事情和RET差不多但是不能混用,RETI只有在中断相关的情况才能用,如果是LCALL或ACALL是只能用RET的
还有一个很特殊的空指令NOP; 连操作数都没有,就这么一个操作码,就让PC+1,什么都没干。这条代码存在的意义上它可以延后一个机器周期,在一些时间需要严格控制的场景可能有用


注意CLR和CPL接地址只有位地址的用法,不会有直接地址的情况
注意ANL C, /bit是把这一位的反码做逻辑与,但是并不会改变这一位的状态,和直接对这一位使用CPL不一样。前面有提到过/bit这种写法是反的意思。
注意不存在ORL bit, bit,比如ORL PSW.7, ACC.7是非法指令。尽管PSW.7和C位置是一样的,但是PSW.7寻址方式是位寻址,而C的寻址方式是寄存器寻址。
除了6条位状态控制指令是单周期的,其他的位操作指令都是双周期的
有些助记符比如MOV, ANL之类的是字节操作和位操作都能拿来用的
| 操作类型 | 指令示例 |
|---|---|
| 字节操作 | MOV A, 30HANL P1, #0FHCLR ACPL A |
| 位操作 | MOV C, 30HANL C, P1.0CLR P1.0SETB CCPL P1.0 |

注意JBC的C不是CY而是clear,所以这条指令在跳转之外还会把这一位置零
保存在内部的东西都可以直接自增自减,无论是通过间接寻址还是基变还是寄存器寻址
注意到这几个算数指令,只有MUL和DIV是4个机器周期,只有DPTR自增是2个周期,其余都是1个周期

所以见到BCD相关的加完必须用DA A

只有第二个是立即数的是2个机器周期的,这种情况第一位只能是一个间接寻址的


注意没有XRL的位运算,想要实现需要绕一下

访问一个字节里的一个位用像是P1.0,PSW.7这样的方式来确定这一位
所有指令及其字节数和周期¶







伪指令¶

ORG是告诉烧录代码的机器这段代码是从哪里开始录入
DB是define byte,用来给ROM存表格的,这里TAB就是table
49H就是十进制73转16进制的

DW如果不足两个字节的就只存到第二个字节里。DS是留出一段空间不用

EQU和DATA之间的话其实EQU的用法多于DATA,所以凡是DATA能用的EQU都可以用,但是EQU可以定义立即数,而DATA不行。所以如果不确定的话全部用EQU即可

在位操作里C和A的地位差不多

这里第一步其实不用,因为PSW.7就是C
程序¶
顺序¶

这个简单,其实之前也见过类似的。第一条代码只是把SP调了一下,还没有往堆栈里存东西。然后呢R0就是一个指针,指向要存放的位置。然后就通过与逻辑分别取了#85H的高四位和第四位来做了ASCII的操作然后存放回去,SP起到一个暂存的作用。其实我感觉就是用这些具体的寄存器作为变量了,不像C那样可以自定义变量,这里你只能用R的那些以及堆栈来当作变量存放东西。
另一种方法

注意XCHD只有这种写法,因此必须用R0做指针。但是其实没有必要用这种复杂指令,多用几个MOV避免非法指令没什么问题。
其实甚至可以将原数和#10H除,商就是高位BCD码,余数就是低位BCD码,由于这里涉及到十进制和十六进制,其实并不是常规意义上的数制转换,而是一种根据它的结构的取巧的计算方法。

务必注意,不能MOV R2, C; 因为C是bit来的,所以你还是得要把它和0相加到A变成一个字节的之后再MOV到R2
我才发现原来不写注释是不需要加;的

为什么这里除以十进制的一百就可以作为BCD码?因为十六进制的低位本身就是BCD码,由于8位只能表达0到256,所以除了之后最高也就是2,完全可以表达。另外要意识到,十进制的100和十六进制的64是一样的,在数字的世界里描述的是相同的东西,计算结果也是一样的,只是这里通过十六进制表达出来,恰好满足了我们对于BCD的需要。而接下来除以十进制的十,由于结果肯定在十进制的0到9之间,所以也不用担心十六进制表示时出现什么E, F之类的越界的东西,直接就可以作为BCD,所以照样可以表达出来
其实没有十进制,它里面只有二进制。DA A实现的十进制实际上是把十六进制的换成十进制的样子,但是本质还是一个十六进制数。


条件¶
用C做条件判断

其实判断是否等于0用JZ和JNZ就行
考试时建议画流程图,而且是一看就知道代码怎么写的流程图,书本上的流程图其实还是不够清晰。

对于有符号数,判断是否为负数就用符号位就行。我感觉这里就是在学C里的if(x), if(x>0), if(x<0)怎么用汇编表达出来

常用到CJNE和JC/JNC(a-b是否小于0),JNB配合ACC.7(数本身是否小于0),JNZ,JZ(数本身是否为0)

对于这个题目我自己写的程序是这样的,我认为我这样写是最清晰的,PPT上给的那个有些莫名其妙
我在写的过程中发现,如何安排相应条件的代码的位置是很重要的,我觉得这样写清晰的原因是,对于特殊的条件,比如大于10和小于5,单独让它们跳出了,然后剩下的相当于else就执行相应的语句再跳到finish。构思如何编排的时候让我想起一个例子就是一群人放学一起走回家,然后先到家的那些就先离队。类比到这里就是先到的就先JUMP了,而整个队伍是走到最后相当于else。而先离队的那些,其实在ROM里离FINISH是更近的,因为它们先回家了,而大部队其实是最后才结束的。注意L5不需要SJMP了,也就前面的需要SJMP。另外条件判断也有一些有意思的特性,就是CJNE你并不需要用到它跳转的特性,不管是否相等都是执行下一条,你最希望得到的其实是C的信息,真正起到跳转效果的是C。而JC和JNC也有讲究,JC其实是能得到<的,而JNC得到的是≥的。因此这里应当和十一比较而不是和10比较,如果和10(也就是#0AH)比较的话那分支就是≥10了,就不符合要求了。由于这里都是离散的,≥11就相当于>10,这样才能满足条件要求。
N路分支的跳转已经有固定代码了

比如R3=1,那么经过MOVC A, @A+DPTR就把PRG1-PRGTBL给写到A了,注意那个是减号而不是杠。然后JMP @A+DPTR,相当于JMP PRG1,就这样就行。

这部分相当麻烦,你需要区分开各种读取数据的方式的范围是多少,这里我进行一下总结
| 指令类别 | 适用指令示例 | 寻址范围 |
|---|---|---|
内部数据RAM(MOV) |
MOV A, 70HMOV 60H, #data |
内部RAM低128字节:00H–7FHSFR(特殊功能寄存器): 80H–FFH |
内部数据RAM(MOV) |
MOV A, @R0MOV @R1, #data |
内部RAM低128字节:00H–7FH内部RAM高128字节(若有): 80H–FFH |
内部数据RAM(MOV) |
MOV A, R3MOV R7, #data |
当前选中的工作寄存器组(R0–R7),位于内部RAM低32字节的某一组(00H–1FH中的8个位置) |
MOV A, #25H |
无地址(数据来自指令操作数) | |
外部数据RAM(MOVX) |
MOVX A, @DPTRMOVX @DPTR, A |
外部数据RAM全部64KB:0000H–FFFFH |
外部数据RAM(MOVX) |
MOVX A, @R0MOVX @R1, A |
高8位地址由P2确定,低8位由Ri(00H–FFH)指定,范围为0000H–FFFFH中的一页(256字节)。注意要设定好P2 |
程序存储器(MOVC) |
MOVC A, @A+DPTR |
程序存储器全部64KB:0000H–FFFFH |
程序存储器(MOVC) |
MOVC A, @A+PC |
程序存储器当前PC附近256字节(偏移0~255) |
| 位寻址空间 | MOV C, 07HSETB 20H.0CLR P1.0 |
内部RAM位寻址区:20H–2FH的128个位(位地址00H–7FH)SFR中可位寻址的位(位地址 80H–FFH中的部分) |
| 程序转移 | SJMP relJZ rel |
当前PC(指令首地址+2)偏移-128 ~ +127字节范围内的程序存储器地址 |


R3能装0到255的,但是由于AJMP每个是两字节,@A+DPTR只能在DPTR的基础上找往下0到255字节的,所以只能覆盖128个AJMP,所以这里R3的取值是从00H到7FH,只有128个
这里RL A是左移A,不过这种方法其实也不是完全保险,需要最高位是0才能正确实现乘2,更保险的写法是CLR C然后RLC A
循环¶
对于循环来说赋初值很重要,不能在没有赋初值的情况下跳到一个循环的内部

这题我自己写的是这样的
我第一次写的时候多写了DEC R1,后面才发现原来DJNZ已经会减一了。
不过还可以优化,这里写了SJMP,但其实可以不需要的,比如DJNZ的那条程序出现了两次,那就可以把它调整到在执行下来的时候本来就会执行,为此可以把JC改成JNC,把操作更多的那种情况(此处指C=1时需要多执行INC 51H)留在“主路”,跳转的功能用来跳过那些不需要做的操作。
这样就更加合理,另外这种分区写代码的方式也可以从C沿用过来。

ADD那条其实也算是循环内一定要做的操作,不过它能够产生是否跳转的判据C,所以必须把它放在跳转语句JNC的前面。而其他那些不影响的,比如INC R0,无论放在JNC之前还是NC后都可以,因为它一定会被执行。多出来的操作放在JNC和NC之间。
注意DJNZ实现了C里for循环的i--和i>0,不过它通常放在循环的最后,这样你的counter就直接写循环执行的次数就行。

这个例子和刚刚写的那个又不同,这里是各做各的,最后用AJMP NEXT胜利会师,不存在说重复使用的,有的比别的操作更多的情况,所以直接用AJMP NEXT就相当于从逻辑上分割成了独立的区块,没有什么复用的部分。这里还是有不少值得注意的地方,首先MOV DPTR, #BLOCK是有井号的,不能写成MOV DPTR, BLOCK,因为它不知道BLOCK在哪。然后呢一开始的话就做了一些归零的工作。
然后循环部分用了MOVX这个也要注意,A取数,只有A可以做像是ACC.7这种事情。至于各自条件INC没什么好说的。而NEXT部分也要很注意,因为你的指针要移位然后计数器也要走,它就都在会师的这个地方完成了。对DPTR自增这样下一个A取的就是高一位的数据,DJNZ完成了对LEN的自减。
还可以用循环来做延时,就是执行很多次,比如要求做20ms的延时,主频是12MHz,对应一个机器周期1us,也就是要有20000个机器周期

其实就是R6从98减到0之后R7才减一次,然后又把R6赋值到98
冒泡排序,如果能看得懂这个那看代码应该没什么问题
DPL是地址,现在通过两层循环来实现从大到小的排序。而且增加了快速结束的条件,就是如果进行完第一轮从来没有交换过,那就直接退出。
每次进行完一次循环,R7会减一,而赋值给R6也会减1。假设R7一开始设为x,那么R7这一层最外层的会运行x次,而R6每次会运行R7次,是随着R7减少也随着减少的。总共要排x+1个数。

查表¶

注意查表用MOVC,从ROM里读数据。立即数用的是MOV,因为它是含在代码里面的,不是单独作为数据存储在ROM的。

注意存放每一个n的阶乘结果需要用3个字节,也就是6位十六进制数,24位2进制数,主要是16位2进制数还不够存9的阶乘


这个程序看起来很麻烦,第一次看你可能不明白为什么要定义A然后又要用DPTR,但这其实都是因为汇编无法像C那样直接把变量存下来导致的。比如MOVC A, @A+PC注定了这事只能由A来干。在汇编里寄存器比如A,就是变量,你不能自己随便定义变量,只能在R0到R7以及A, B, DPTR里选,一些特殊操作又只能用A和DPTR,所以就会出现这种需要暂存到其他地方的现象。
如果写成C其实很简单的事情
汇编里用B存放ZHANG,为的是到时CJNE比较,这里CJNE没有用到C而是用了jump的功能。CJNE不能接ZHANG所以需要额外用到B。然后R7相当于这里for循环定义的i。DPTR用来装地址,由于不能用MOVC A, @DPTR,必须要@A+DPTR或者@A+PC,所以就不能像C里面那样用address一个变量就能完成寻址以及地址的记录。说实话我觉得这里@A+PC也有些多余,干脆就用A表示0,1,2等等然后用@A+DPTR就好了。哦不过它希望用DPTR表示找到的那个地址,这样就只自增A的话,就不需要自增DPTR,只需要在找到的时候让DPTR加上A就行,注意要先和低八位加然后再对高八位用一个带进位加法。当然用这里@A+PC来寻址方法也行,只不过这样就要数一个16H。由于只能用A存ROM里取到的数据,而寻址又只能用@A,所以需要把A暂存到堆栈里去,这也是一个比较麻烦的事情,C里就没有这个要求。其他的就没什么好说了
注意就算找到了也要做POP ACC,PUSH和POP必须成对出现
我觉得汇编和C还有个不同就是汇编你知道程序和表格都是放在ROM的,在位置近的地方直接从程序走几步就到了,可以用这种方式查表,但是C里你只知道数组初始的地址就是数组名,不知道它放在什么地方。这也是单片机内存小所以可以这么搞,所以才有@A+PC这种寻址方式
汇编还有一个特点就是这里就不用区分是全局变量还是局部变量,都是看你用的是哪个寄存器,用途是你自己分配的
子程序¶
所谓子程序相当于函数,就是你想用就调用的
子程序要写标号
子程序以RET结尾

注意LJMP无法RET,只有像是LCALL或者ACALL这样的调用才能返回,JMP只是跳转不会帮你把PC压到堆栈去再取回来

这里注意JZ到SBR1,但是RET是回到ACALL的下一个而不是回到JZ的地方。
由于A=10H是十进制16太大,我先演示A很小的情况再往上推广
假设A=02H,那么先减一变成01H,跳过JZ,来到ACALL SBR,此时PC指向SBR1被压入堆栈。然后调用SBR的时候再给A-1,变成00H,那么就JZ来到SBR1,给DPTR+1得到0101H,然后RET是取不久前被压入堆栈的PC的地址,就来到SBR1,给DPTR加1为0102H,然后到RET,这时RET的含义不同,变成了回到主程序,于是得到DPTR为0102H
假设A=03H,减一变成02H,ACALL SBR使得指向SBR1的PC被压入堆栈,调用SBR,减一变成01H,ACALL SBR使得指向SBR1的PC被压入堆栈,调用SBR,减一变成00H,JZ到SBR1,给DPTR+1为0101H,然后RET的时候把第二次被压入堆栈的PC取回来,于是又来到SBR1再给DPTR+1成0102H,然后遇到RET把第一次被压入堆栈的PC取回来,来到SBR1再给DPTR+1成0103H,再遇到RET,回到主程序,此时DPTR为0103H。
由此可见,A是几就给DPTR加了几,所以A=10H理论上可以把DPTR变成0110H,不过这样其实风险很大,因为单片机的堆栈没多大,如果用的太多可能把高位的那些寄存器都用到了甚至用完了,那就会出问题。所以不要用递归
经典程序¶
案例1:小于等于10,10到20,大于等于20的条件语句。注意这里你就直接把它分成不同区块的代码,没有必要为了让代码尽量少而利用它顺序执行的特征来简化代码,先熟练掌握这个写法。
案例2:数组求和,从30H开始放16个数,用到循环和16位加法操作,也算是比较经典了
案例3:延时程序,熟练掌握写法,注意DJNZ放在最后面,而且有两层
案例4:查表找值,用好MOVC和DPTR
系统扩展¶
ROM和RAM扩展¶
这张图是ROM扩展的时序

一个机器周期包含6个STATE(对应时钟XTAL的STATE1到STATE6),每个STATE包含两个P(P1和P2),每个P是一个时钟周期。P0和P2是地址线,其中P0是地址数据复用的,麻烦一些。在ALE高电平期间,P0做地址线,输出地址低八位,也就是PC的Low(缩写成PCL),由ALE信号锁存。而接下来-PSEN低电平的时候,P0是数据线,可以从外部扩展ROM中读信号。注意PSEN是program store enable,是和ROM相关的,而RAM相关的由RD和WR控制
下面是RAM扩展的时序,第一张是RAM扩展的读的时序,第二张是RAM扩展的写的时序



这个是和ALE配合的锁存器,-OE是output enable,当它为高电平的时候处于高阻态Z。当-OE为低电平的时候是允许输出的,此时看LE即latch enable,当LE为高电平,Q原样地获取D,当LE为低电平,Q状态不变,实现锁存
ALE信号接LE,当ALE高电平,P0输出地址低八位,373获得,随后ALE低电平,373把地址低八位锁存,P0再去输出数据

型号27后面的数字除以8就是容量,比如16对应2K,32对应4K
其中chip select是最麻烦的,要用它来确定你能扩展的64K空间里,占的究竟是哪些空间。因为P0和P2总共提供十六根地址线,可以扩展64K,但是比如说你扩展的ROM只有2K,那这2K放在64K中的哪里,就要由CS定。其余的都是无脑接的,数据线接数据线,地址线接地址线,-OE接-PSEN
RAM也是类似的

-OE和-RD接,-WE和-WR接
注意8031外部扩展ROM的时候注意要有0000H
如果扩展多个ROM或RAM,注意分配的地址不能重叠
ROM扩展¶


这里是扩展两个32K的ROM,用A15配合非门来选择是访问U1还是U2。在这里当A15=0,就访问U1,而A15=1就访问U2。于是可以等效看成最高位是0是U1的地址,最高位是1是U2的地址

这个是只用了一个32K,A15直接不用了,这就造成无论A15是0还是1,只要A0到A14相同,就访问的是扩展ROM里的同一个位置。比如0000H和8000H访问的是同一个。这样也能用,不过你就不要用8000H及以上的地址了,以免误以为那是另外的存储空间

每个ROM是8K
如果是这样的话就是用高三位来接一个译码器,当(二进制)高三位为000就可以用U1,高三位为001就可以用U2,高三位为010就可以用U3,高三位为011就可以用U4。而其他的是一个都访问不了。由于转换成十六进制时是二进制高四位变成十六进制最高的一位,所以你看U1对应的A15A14A13A12只能是0000或0001,所以变成十六进制就是0或1。而低12位任取,所以可以覆盖000到FFF,所以U1对应的是0000H到1FFFH

这个也好理解,就是00X(+后13位)对应U1,01X(+后13位)对应U2等等,这里就是A13无所谓是0还是1
主要是看4位十六进制数的第一位是啥。你看A13和A12是在前四位的,当A13取0的时候,A12可以是0,1,所以对应十六进制的第一位是0或1。而A13取1的时候,A12也可以是0,1,所以对应第一位是2,3。
这几个例子都是低位对齐的,这样的好处就是低位部分是连续的,如果你是A1到A13对齐的话,那它就不连续,需要很多跳转,就很麻烦

线译码就是把译码器给省了,就单独控制,这样硬件简单,但是软件方面就比较麻烦
高三位011唯一对应U1,101唯一对应U2,110唯一对应U3,其余的都会出现两个都访问的情况就很乱套,比如000那就全都访问了
所以线译码写软件就要比较小心,只能用那些能区分开的地址,不然就乱了
总结一下全译码就是没有并联的都接到译码器了,部分译码就是只用了够用的线接到译码器,线译码就是没有译码器

这个要记忆,注意复位电路部分,那个开关K是两端各接一个电阻,一个接到5V,一个接到地。

RAM扩展¶

外部扩展RAM的唯一区别就是控制信号-WR接-WE,-RD接-OE

其实只有一个ROM一个RAM的话是很简单的,因为要用ROM的时候-PSEN低电平有效,而-RD和-WR是高电平无效的,所以RAM会变成高阻态,没法操作。这里要求全译码,那就全都用上就行。

线译码由于RAM要用到A14,所以只能选A15,当然其实你给ROM单独配,给RAM单独配也没关系,甚至-CE都接0都行,因为读写已经由-RD, -WR, -PSEN决定了,而且只有一个ROM和一个RAM要用的时候只能选它。
另外注意这里EA上面应该是有横线的,这里图方便没画
I/O扩展¶
由于ROM和RAM扩展需要用到P0和P2做地址,P3又有-WR和-RD,所以其实也就P1是纯粹用作I/O的,这可能就导致引脚不够用
读和写就对应I/O的输入和输出
输出扩展


这样在-WR从低电平跳到高电平的时候就可以把数据存到LS273
输入扩展

这个看起来复杂其实很简单,就是三态门,输入输出相同,由G控制,低电平有效。注意LS244和HC244功能一样,LS和HC只是制作工艺不同

当这里-RD为低电平,这个HC244就有效了,就可以存外面传过来的数据,如果-RD高电平,那它是高阻态不产生影响

图中LS273是上升沿触发,LS244是低电平触发。当A15为0的时候经过非门变成1,那么经过或门输出一定是1,此时不可能触发。当A15为1的时候,或门输出结果和-WR或-RD相同,此时就和之前完全一样,当写信号来的时候在-WR有一个上升沿,于是可以把D0到D7保存到Q0到Q7,而读信号来的时候就低电平触发LS244,可以把1A,2A给存到1Y, 2Y。所以A15为1的都有效,从8000H到FFFFH的效果都是一样的,因为其他地址线你都没有用啊所以是什么都没关系

注意因为发光二极管一侧接VCC,所以Q为0的时候二极管会亮,比如01010101的时候1357会亮,10101010的时候2468会亮。建议都是这样,不要把二极管反过来,因为一般是0的带载能力更强

我想的另一个验证方法是ADD A, #1然后根据C来判断,不过可能就没这里这么简洁
这样操作是比较低效的,因为是在一直查询,如果能改成中断的话,就不用占用主程序了,还可以运转其他的操作。
总线扩展的结构都是这样的

比如CPU是8051,可编程I/O接口是LS273,I/O设备是DAC。当然如果单片机本身已经够用了,就没必要外接别的东西了
外部扩展芯片的时候,芯片上有CLK,如果没有特殊要求接ALE即可
然后其实其他的芯片,告诉你到底什么情况下是输入输出,以及怎么控制它的状态,你就按要求去写软件就行了,其实这个是简单的
中断¶
子程序的调用是有计划地被打断,显式调用。
中断是不可预见程序执行到何处发生的,但是可以提前写好中断的子程序,这样如果发生中断就可以先去处理了再回来继续执行
与中断相对的是查询(或称轮询,polling),就是你时不时检查一下有没有出问题,这样比较耗运算资源,而中断的反应是即时的,而且引发中断的事情没有发生的话,是不干扰程序运行的
中断源是引起中断的原因,比如按键,定时器,断电保护等
开中断时CPU才能响应中断,相对地关中断就把中断给屏蔽了不去处理
中断有优先级,高级的中断可以打断低级的中断


IE是interupt enable,控制中断的开关。IP是interupt priority,控制中断的优先级,不过只有两个级别,你就算把所有的优先级都调成了1也只能响应一个。TCON是time control,SCON是serial control
入口地址就是发生了这个中断就自动跳到这个地方开始执行相应的中断子程序,所以ORG 0000H要接一个LJMP MAIN,不能直接写主程序,不然会占用这些中断入口。这些中断入口一般也是直接接跳转到中断子程序的指令,比如ORG 000BH LJMP T0_ISR,ISR是interrupt service routine,中断服务程序
中断控制示意图¶

RX是receive,RI=1表示51串口收完了, TX是transmit,TI=1表示51串口发完了,经过或门接。EA为0的话就都屏蔽,然后也可以单独控制EX0, ET0等是否响应。然后IP可以控制优先级,其实有一个自然优先级(就是左边这几个的顺序,外部中断0最优先,串口的最低),如果全部接1或者全部接0是一样的,相当于你就交给硬件设好的那个优先级。如果你希望某一些优先级更高的话就把它们的IP弄到1就行。
触发模式方面外部中断0和1是可以选择低电平触发和下降沿触发的,这个一般用SETB IT0或SETB IT1就行,是可以位寻址的
其中TCON和SCON都是特殊功能寄存器用来锁存中断请求标志

注意串口引发中断的是RI或TI,而且处理完中断你要在代码里手写CLR RI, CLR TI,无论有没有开串口的中断。
中断的实际处理流程

就是每个机器周期的S5P2都会查一次中断标志位,如果查到是1,就会把它存起来,那么下个周期会查这个存起来的结果(轮询,polling),不被block就触发LCALL。下面这个图表达一开始查到已经是一个周期要结束了,下一个周期把当前指令执行完,然后调用LCALL用了两个周期,从一开始的那个周期算第一个周期,得要到第五个周期才真正开始执行中断的程序(routine)

然后列出来的那三条是会阻碍这个过程发生的因素,这个provided可以理解为“前提是”,最后那句翻译过来就是前提是这个硬件产生的LCALL不被任何下列情况阻碍。然后下面那三种情况首先是如果同级或更高级的中断还在进行那就先不处理(同级里面自然顺位的不影响这里,自然顺位是两个中断同时发起的时候决定先处理哪个),然后第二点是当前指令经过这个周期还没执行完,那就不会对中断做轮询,而是当当前指令执行完再说。第三点就是当前在执行RETI或者修改IE, IP寄存器,那也会先等这些搞完,然后注意得要再延后一个机器周期才开始执行轮询(poll)。
然后PPT上是这样写


再补充一个点,哪些中断标志位要手动清零。
| 中断源 | 标志位 | 触发方式 | 是否自动清零 | 是否需要手动复位 |
|---|---|---|---|---|
| 外部中断 0(INT0) | IE0 | 边沿触发 | ✅ 硬件自动清 | ❌ 不需要 |
| 外部中断 0(INT0) | IE0 | 电平触发 | ❌ 不自动清 | ✅ 必须手动 / 外部撤销电平 |
| 外部中断 1(INT1) | IE1 | 边沿触发 | ✅ 硬件自动清 | ❌ 不需要 |
| 外部中断 1(INT1) | IE1 | 电平触发 | ❌ 不自动清 | ✅ 必须手动 / 外部撤销电平 |
| 定时器 0 中断(T0) | TF0 | 溢出触发 | ✅ 硬件自动清 | ❌ 不需要 |
| 定时器 1 中断(T1) | TF1 | 溢出触发 | ✅ 硬件自动清 | ❌ 不需要 |
| 串口 UART 中断 | RI(接收) | 接收完成 | ❌ 不自动清 | ✅ 必须手动清零 |
| 串口 UART 中断 | TI(发送) | 发送完成 | ❌ 不自动清 | ✅ 必须手动清零 |

这段话意思是在某一个周期查到有中断的时候,如果因为各种原因阻碍了这个中断响应,那么相当于没有影响,下一个周期会接着查,查到有且不被阻碍那就执行中断,如果你把这个标志位改了,导致下一个周期查不到,它不会记得之前查到了但没处理的状态。不过这些知识点就太细了,是手册里的东西,应该不会考
中断的锁存器和控制¶
TCON的组成

TR1和TR0在定时计数器那里讲

中断控制有IE和IP,其实就相当于按钮用来设置状态,很好理解


设置也很简单,用到SETB置1然后CLR清零

如果主程序执行的时候已经在执行了,必须等这一条指令执行完才去响应中断
若执行指令是RETI或是访问IE或IP的时候,执行完该指令至少再执行一个指令才响应中断,所以如果你一直在对IE或IP操作,那它也无法响应中断
响应中断的时候会置位优先级触发器,自动清零TCON的那些,但是不会清零SCON的TI和RI。另外响应中断也会保护断点,把PC弄进堆栈后面恢复
RET只会做PC恢复的那一步,RETI会完成中断相关触发器的处理,所以不能用RET替换RETI。RETI多做的是清除中断优先级触发器,这个触发器是用来封锁更低优先级的中断请求的,经过RETI就可以把这个给解除,如果用的是RET而不是RETI,那这个触发器就一直封锁着,没法处理低优先级的中断。然后RETI还会告知中断已经处理完毕,而RET不会,就会导致系统误认为还在处理中断

所以中断子程序必须用RETI而不是RET,不然它会认为一直还在执行之前的那个中断,这样下一次这个中断再次出现它就不会响应了。
五个中断程序的入口地址固定,而且那几个入口之间没有多少空间,所以如果中断程序较长需要先来一个LJMP跳到空间充裕的地方写
中断相关代码¶
实际放的东西是这样的

外部中断¶

外部中断比后面的定时计数器和串口简单多了,也就查询的时候需要用到IE0,而且IE0可以位寻址,不过这里没有说出来

注意外部中断如果是电平触发需要手动清掉中断标志位
定时计数器¶
用不确定何时发生的外部事件触发,就是计数器,用内部的固定频率的脉冲来触发,就是定时器

注意外部脉冲触发的原理是在第一个周期的S5P2里采样,是高电平,第二个周期的S5P2里再次采样,得到低电平,就记录为一次,这意味着完成一次计数至少要2个机器周期,所以能计数的最高频率是振荡频率的1/24,如果是1MHz,那外部输入触发的最高就是2μs计一次
GATE是自己配置的,-INT1是外部管脚,设GATE=1相当于屏蔽外部管脚
上面这个图只是一种模式的情况,还有其他模式,具体看设置的相关寄存器
定时计数器TMOD寄存器不能位寻址,一整个字节连在一起,需要全部想好之后把一个字节弄进去做好配置

GATE是TMOD.7
有两个定时计数器,T1和T0,这个0和1也是对好的,比如1就对应TR1和-INT1,0就是TR0和-INT0
GATE和C/-T已经知道,就看M1和M0

13位计数器和老版本兼容有关,自动装载的8位计数器是指计完一轮它可以从头开始循环计数,而16位的那个就不行你得重新配。而方式3注意用不了T1
可重装载指的是8位的TL0计数到全1之后就把TH0的值复制到TL0继续计数到全1,这样循环,这个过程不改变TH0的值,这样就意味着你可以做一个低于256的一个循环计数

第三种模式下原本属于Timer1的TR1和TF1被用来做TH0的相关设施

注意只有Timer0有mode3,Timer1是没有mode3的。把T0设为mode 3,T1就没法控制了,它就自己在那跑,只有你需要用T1做串口波特率发生器且需要用两个8位定时器的时候才用mode 3,否则就T0和T1分开就够了,没必要这样搞,这个功能在实际开发非常不实用。

当T0设为Mode 3,TL0自己做一个功能完整的8位可计时可计数的,TH0做一个只能计时的而且只用TR1控制的,而T1就是按你之前设置的模式继续跑下去,如果T1设为模式0那就是13位的,T1设为模式1那就是16位的,T1设为模式2那就是8位重装载的,此时它不再产生标志位来说明它计数完一轮,就一直这么计数下去。如果把T1设为模式3,那么它就中止。
TCON是可位寻址的,前面中断也讲到过


注意是没有用TL的高三位,这个配起来比较麻烦可以不用这个模式
方式1就是最开始看的那张图,可以自己配置TH1和TL1作为初值
方式2是下面这样的,前面控制的是一样的,实际计数用的是TL1,8位,0到255,当到达256的时候触发TF1=1,然后把TH1的值弄到TL1作为初值

方式3是这样,将0的拆开两个来用,但是TH0那一边的控制只能用来做定时器,而且只能用TR1控制了,然后进位标志用了TF1

52的定时计数器不考

另外如果是外部扩展的减法计数器,假设也是16位,如果要计数65536,一开始应该写0,这样它会回到65536,然后再减65535次得到0。如果是加法十六位,也是写0
我自己写了一个配定时计数器中断要用到的寄存器汇总

最上面是说明各个section的功能,然后第二行就是把它那个图用最简单的方式给拼起来了,既是执行的顺序也是写代码的顺序。其中TH0和TL0是计数器初值,各自8bit,而TF0是计数器0的溢出标志,如果想要用查询模式就要MOV C, TF0然后JC或JNC来进行条件判断。后面ENABLE和PRIORITY就是中断的使能和优先级的设置。第三行是与各个寄存器相关的常用操作,其中C/-T无法位寻址,它在TMOD里,我认为这个是最麻烦的,所以把TMOD的具体内容也写在下面了。类似地GATE和M1, M0也都在TMOD设置。而剩下的都用SETB或CLR来设置即可,一般也用不到CLR
这个图的好处在于,你写代码不会不知道还差哪些,因为这里已经把一个定时计数器要用到的所有寄存器都包含在里面了。上课介绍的方式有一个弊端就是你只知道这些寄存器的存储位置却不知道这些寄存器的逻辑关系是什么,如果能看着这个图来了解就清晰得多
当然这个图还不足以解决所有疑问,比如那几个模式的处理方式就还没有在这个图里写出来,不过如果对于已经熟悉那几个模式的人来说,只看这个图也就足够明白怎么设置了
只画出定时计数器0就够了,没必要把1给画出来,1无非就是把这里所有的0换成1就是了
通常来说你只用一个定时器或计数器,那在TMOD里用不到的那个全0即可
程序例子¶

注意不想用完13位或16位需要用2的相应次方减去你打算计数的次数,比如这里是500次就减去五百,然后换成16进制数赋值给TH和TL,注意对于13位的TL的高三位是用不到的

所谓周期为2ms,其实是1ms翻转一次,也就是计时1ms来一次中断

如果用查询方式而不是中断,就不需要配置中断了,但是需要手动给标志位清零。


流程就是判断定时还是计数,然后选方式,然后算好初值,然后就配TMOD和TCON,开中断,写中断程序

最好加个LJMP
演示一个给高电平计时的程序,这个时候GATE就不是常0了,而是用INT1来启动


务必注意这里等待的指令是用JB和JNB P3.3(-INT1) $来原地跳转的,这和之前的SJMP $不同,这是有条件的等待
JB P3.3, $的目的是如果一开始是高电平那就先别开始计时,等低电平来了之后再启动TR1,但此时仍未开始计时,直到-INT1变为高电平的时候才开始计时

这里是演示设置两个中断,一个是外部中断,一个是定时器T0
主程序只写了配寄存器,之后直接SJMP $

我这里补一个不同时间翻转P1.0的程序的对比(都假设晶振12MHz),最好用的是可重装载的,这样你就不用自己在每个中断触发时再装载,但是可重装载的只能搞256μs,如果要你搞50ms的,那方式1可以提供最多65536μs也就是65ms的计时,用方式1方便。但是如果要你用2s的翻转,那就连方式1都不能支持了,需要用上外部循环。
串口¶
并行传输的一个问题是,如果要和时钟同步,那就要严格保证传输的路径是一样长的,不然会有延时,导致同一时刻接受到的信号可能不是你原来传的信号,串行传输不会有这样的问题,就一根线。所以长距离传输一般也是用串口
单工指的是只能收或只能发(比如只有人听电台,没有电台听人),双工模式就是收和发都可以。双工又分为全双工和半双工,半双工指的是收的时候不能发,发的时候不能收(比如对讲机,一个讲的时候另一个只能听)。全双工指的是可以同时进行收发,8051的串口是全双工的。全双工指的不是RXD可以做收和发,而是同一时间,RXD和TXD都在工作
串口分同步传输和异步传输,同步传输就是和时钟对齐,51做不到,只介绍异步传输
异步传输通过通用异步收发器UART(universal asynchronous receiver/transmitter)实现
由于异步传输没有时钟,所以需要比较麻烦的规定,首先收发的速率得是一样的,比如都是9600bps(bit per second),然后数据格式是固定的。

用低电平来标志数据开始,所以在不传数据的时候是高电平
最低有效位LSB全称是least significant bit, 最高有效位MSE是most significant bit。MSE是D7,LSB是D0
第九位可有可无,要事先商量好有没有,如果有的话是什么含义,比如可以设为奇偶校验位或者其他东西
51的串口接起来很简单,就TX接另一台的RX,RX接另一台的TX,然后共地


注意地址是共用的,但其实是不同的硬件。当你把A放进99H,它就经过发送缓冲器串行发出去了,当你把99H的数移到A,它是从接收缓冲器取数。接收缓冲器其实有两个但是用的时候不关心,它有两个是为了防止你还没取的时候又接受一个导致覆盖了,有第二个的话它就会存到第二个里面。这种存储方式称为乒乓存储,其实手机里也有,比如系统更新的时候并不会直接把原来的给清掉,而是现在另一块里更新好了检查没问题了才去清除原来的系统,相当于做了一个备份,包括手机恢复也有用到这样的原理
你自己写不需要用99H,直接用SBUF就行。
然后一个误解是你软件在写的时候一个时候只能执行MOV A, SBUF或者MOV SBUF, A,这不是没法用到全双工吗。但是事实上你执行完一条立刻执行第二条,你发的时候其实还没发完,但这并不会报错,因为发和收是两个东西,他们可以并行工作。不过一般不会这样写,如果用轮询的话应该写成下面这样
中断的时候讲到过TI和RI,由于串口中断来的时候不知道是T还是R导致的,所以需要你自己去查一下是哪个,然后手动清零,注意中断里TI和RI是不会自动清零的

注意SM0是SCON.7,RI是SCON.0
方式0通常不用,方式1和方式3就是8位(无校验位)还是9位(有校验位)的区别,都可以调整波特率,波特率和T1溢出有关。方式2的波特率只有两种,fosc是晶振频率也就是时钟频率
方式0其实是同步的,RXD用来收发数据(收和发的时间隔开),然后TXD输出时钟,来指示下一位发出的时间,每来一个触发沿,RXD就输出下一位的东西。
发送的时候,如果选择了9位,那么第9位就是TB8,你自己设置好再让它传。而接收的时候如果有第九位会放在RB8里面。总是有的那八位就是进SBUF的

REN是receive enable,这一位设为1才能接收
SM2一般只在方式2和方式3考虑。当SM2=1且RB8=1时才会触发中断,如果SM2=0且RB8=0那么不会触发中断。
假设A主机想要给B和C从机通信,比如A给B交代一个任务,这和C没什么关系。此时A会进行广播,B和C都能听到那8位,但是只希望B响应。那么我就让B的SM2为0,C的SM2为1,而且发的第九位是0,那么就可以实现C收到数据但是看到第九位是0,就不会发出中断,于是就不处理收到的数据。
更加完整的流程是这样的

除了配置SCON,还有一个是可以调整串口通信的

对于8051来说,波特率=比特率(其他机器可能有差异,但这里我们不考虑),见到波特率你就等价看成bps就行
方式0的时候波特率是晶振频率/12,因为它就是按机器周期来的,下面提到的focs就是晶振频率
方式2的时候,如果SMOD=1就是focs/32,如果SMOD=0就是focs/64
方式1和3的波特率为\(\dfrac {2^{SMOD}} {32}T1溢出率\)

方式2波特率为\(\dfrac {2^{SMOD}} {64}fosc\)

现在解释那个T1溢出率怎么回事。当T1用来做这个串口辅助的时候,设为方式2,也就是8位重装载的那个,而且是定时器模式,而且disable了T1的中断,就T1纯用来做这个串口输出的一个波特率发生器。定义T1溢出率为\(\dfrac {fosc}{12\cdot(2^8-初值)}\)
实际应用的时候会直接给表格,不用自己算这个东西,但是公式可能需要记忆一下。

举一个用中断的例子

要根据要求的波特率配T1,所以也要用到前面的图
串口比较麻烦的就是还有个SMOD要记得,它不能位寻址,只能整个赋值给PCON,然后SCON就和TMOD那样具有相同的地位,虽然SCON可以位寻址,但是还是推荐用MOV一次配好,一个一个配太麻烦了
如果使用轮询模式,无需配置中断相关的ENABLE和PS,只需要自己查询TI和RI,比如用JB和JNB来判断是否要执行下一条语句,其实我感觉一般用轮询是比较简单的
方式1应用¶

这里关于奇偶校验位缺了很多前置信息,直接看看不懂
51的那个奇偶校验位P满足的规则是,当A里的1个数为奇数,P=1,当A里1个数为偶数,P=0
ASCII码是0到127,只需要用到7位也就是D0到D6,所以在八位里面的D7一定是0。为了利用好这个D7,给它单独赋值,起到奇偶校验的功能。方式1是发送8位,方式2是发送9位(8位加上一个附加信息的位),而在这里我们是7位加上一个附加信息的位,总共八位,用的是方式1。
这个第八位我们希望实现包含它在内的8位,所有1的数目是奇数。这就意味着,如果D0到D6有奇数个1,那么D7应当为0,如果D0到D6有偶数个1,那么D7应当为1。
在一开始MOV A, #ASCII的时候,由于D7=0,所以1的个数由D0到D6决定。当D0到D6有奇数个1,P=1,而我们希望D7为0,所以需要把P取反。(由此可以看出,如果把P当作校验位,应当是偶校验位)
这个程序能自己写出来就算是中断,定时计数器,串口过关了

此处甲机和乙机用的都是查询模式而不是中断模式,因此没有配置中断。查询模式体现在它通过JNB TI, $来不断查询,不能执行其他的代码

多机通信¶
用方式2和3进行多机通信,其中第九位通常就是用来确定到底是和哪一台通信的,我前面已经解释过这个了

第一次发地址,匹配上的那一台从机把自己的SM2改变,然后就可以处理第二次发的数据了,没匹配上的那些就不响应

意思是在模式1接收过程中,若SM2=1,只有当接收到有效的停止位时,才会置位接收中断标志(RI)并触发接收中断;若停止位无效,即使接收到数据位,也不会触发 RI,数据会被丢弃。若SM2=0(默认常用状态),则停止位是否有效不影响中断,只要接收到 8 位数据就会触发 RI,这是常规单设备通信的设置。
模式2、3可以用于多机通讯
数模转换¶
考最后两道大题,系统扩展和系统设计
不考12位的,只有8位的,掌握0809等
A/D是analog to digital,模拟信号转数字信号。D/A是digital to analog,数字信号转模拟信号
数字信号转模拟信号举例,比如一个八位的二进制数过来,对应十进制的128。那么我把0V到5V进行256等分,接收到这个128我就输出一个2.5V的,就这样一一对应
DAC就是digital to analog convertor,ADC就是analog to digital convertor
D/A¶
对于D/A转换,n位数字量的分辨率定义为最大输出/2^n,其实就是单位数字量变化引起的输出量的变化。比如10位的数字量,最大输出10V,则分辨率为10V/(2^10),分辨率就是1 LSB,你可以写1 LSB=10V/(2^10)


只用P1因为P0和P2用来做地址,P3用来输出读写命令之类的,只有P1比较闲。然后这个程序挺巧妙的,我没能想出来,主要没想到它的工作流程,就是下去然后循环完进入下一个循环,循环完跳回一开始,确实以前没见过这样的。然后这里用的是JNZ而不是CJNE也很巧妙,因为确实到达FFH之后下一个就变成00H,其实是出界,这个时候再进入下降的循环,这样就又降回FFH
注意这里延时程序的应用,如果一直运行可能太快了

加了一个锁存器就是这样的,273是用来I/O扩展的。A用来存后面@A+PC需要跳转的量,然后R0是计数器,DPTR用来记录输出的地址,这里用到了扩展所以要用DPTR。所谓任意函数发生器就是之前已经采样好了,存进下面的表格,你就查表一个个输出就行。这里用了PUSH和POP对原来的A进行暂存,但其实你另外找个变量也行,然后这里注意让A输出得要MOVX @DPTR, A。剩下的没有什么
这里没法用MOVC A, @A+DPTR,因为DPTR另有用处,所以需要用MOVC A, @A+PC,所以A一定会用来存变址以及从表格里取回来的数。你可以把变址放另外一个寄存器,这样就不用PUSH ACC了,这里图上PUSH A不规范。
这里其实还有一点问题,因为它对A还自增了FFH次,这会导致A原来是0BH超过255又绕回来了,是有问题的
D/A转换器是另外配的,不是51内置的


当-LE1=0,则第一个寄存器锁存,当-LE1=1,则第一个寄存器的输出和输入相同,对第二个类似
第一种接法,让-LE1和-LE2都为1,相当于不用这里的两个锁存器,然后把P1接到输入端这样就行,P1本来也有锁存器。这样就是锯齿波发生器的那种样子

第二种接法,让-LE1和-LE2中的其中一个为高电平,另外一个在一些时候为低电平,然后加入写信号和片选(page)信号,就可以用到其中一个锁存器,相当于任意函数发生器那个例子的红色框部分

这里是-LE2永远为1,而-LE1由A15和WR控制
第三种接法,通过-WR1和-WR2控制两个的锁存状态,用-CS和-XFER来做片选

A15=0选中第一个寄存器,A14=0选中第二个寄存器
双缓冲型通常用于多个同步输出的时候

先用FDH控制把数据写进第一个DAC0832的第一个寄存器存着,再用FEH把数据写进第二个DAC0832的第一个寄存器存着,再用FFH一起控制第二个寄存器,就可以实现同时输出了
下面是12位的,考试不考*
如果想要更多位的DAC,必须要用这种同步的接法,不然你先传低八位再传高四位,那传高四位之前的状态就不是你想要的状态,要避免这种状态出现,所以需要用这种方式来锁存
可以这样搞

想要实现12位的已经有专门的芯片DAC1208了

当BYTE1为高电平,假设其他都是高,那么8位的通了。当BYTE1为低电平,则8位被锁住,后续只能让4位的或12位的打开

接法是这样,其中D0到D3和D8到D11是复用的,当A0=1且-WR1=0,写入第一个八位(此时低四位也被写了,但是数据无效),当A0=0且-WR1=0,高八位不动,正确的低四位写入锁存器同时12位寄存器也有Q=D(因为A0=0相当于让XFER有效了,而且WR信号用的是同一个),于是就可以让之前存的八位和新存的4位同时写给12位。
在这里4位锁存器看起来没什么用,因为这里是单个12位,所以它只用到了直通模式,可有可无,如果要做多个D/A,那就有必要用到它了,因为多个D/A的时候要全部存完才同时让12位寄存器开,此时就需要用到低4位
A/D¶
A/D信号就是模拟信号变成数字信号,模拟信号比如现实中的声音和光,被记录在手机里就是变成了数字信号。
介绍ADC的两种类型,逐次比较型和双积分型

本质上引入了负反馈,一种实现方法是SAR里存一个最大的数比如FFH,然后经过D/A转换器输出到A去做比较,如果比ux大,比较器输出一个低电平,经过逻辑控制电路给SAR减1,然后再把这个值转换成模拟信号再去比较,如果还是比ux大那就继续减1,知道SAR的值转换成模拟量刚刚小于等于ux的时候,逻辑控制电路就认为这个值可以了,就让输出缓冲器把这个值输出出去

先清零,第一阶段ux接通,我们假设ux不变,然后运放两端是虚地,这样R1两端的电压是恒定的,于是就这么恒定给C充电,充电时间我们事先设一个值,这是已知的。接下来把ux断开,然后接一个和ux正负性相反的电压,如果ux是-的就接Vref,如果ux是正的就接-Vref,Vref的值也是已知且恒定的。这样就进入第二阶段,此时C就放电,当放电到0的时候再把开关断开,记录下第二阶段放电的时间。

然后运算出来ux就行了,然后进行输出
对于变化较快的信号,需要加采样保持器,就是取采样的那个点的值保持不变,不然积分出来斜率就不是固定的了,得到的结果也不是采样点的那个值

R0做计数器,R1做指针。P1设为#FFH的原因是,需要把它接地的三极管截止,否则外面无论输入什么信号都只能获得低电平,这一步是用P口的时候需要做的初始化。然后P2.0和P2.1是控制START和EOC信号的。时序是下面这样的

当-EOC为低电平,说明已经采集完毕,你可以从P口取了。EOC是end of convertion的意思。然后START是要手动复位的,而EOC是它自动复位的
注意START和EOC是针对一次采样的信号,所以它们的复位也要在循环里面。JB P2.1, WAIT就是等待-EOC信号变成低电平,那就可以去取了,之后就把P1的弄进去就行
注意-START在CLR之后要DLY然后再SETB

第二种接法,就是加了一个模拟开关(数据选择器),模拟开关就是用P2.2,P2.3,P2.4的输入来决定是八个通道中的哪个通道和输出直接相连

这个程序还是很综合的,不要在一个里面把所有的都完成,多写一些子程序。比较重要的是八个数相加需要两个八位,所以不能简单DIV AB了,又注意到8相当于右移三位,所以就用右移来简化了,这里忽略了余数

12位的A/D转换只需要两个寄存器把数据存起来就行了,不需要像D/A那样做一个同步的,因为这里转换完了锁存好了,数据就随时取都行,只要在下一次-START开始前搞定就行
介绍集成ADC
ADC0809

注意此处START DOC OE等都是高电平有效,所以和那些连接的时候需要加一个非门

这里EOC没接,而是通过一个延时来等到它处理完,因为手册已经给出最大的转换时间了


这里只演示了一个通道的处理方式,没有写循环,否则应当给DPTR移动到下一个通道而且也多设一个存数据的指针
键盘和显示器接口¶
考的很少,了解独立式键盘和矩阵式键盘的差异和矩阵式键盘,以及动态扫描显示的原理即可,专用芯片都不考
键盘¶
键盘分为编码式和非编码式,编码式键盘带译码器,硬件完成编码。非编码式内部没有译码器,只能返回按键在第几行第几列,由软件来明确这个位置对应的编码。微机原理里用非编码式的多,这里只介绍非编码式
非编码式的又分独立式和矩阵式,独立式就是一个口线接一个按键,读取口线状态知道是哪个,矩阵式按键会对按键做二维行列的排布。
要解决按键机械抖动的问题,理想情况是没按的时候高电平,按下低电平,放开又回到高电平,但是实际上在按下和松开的时候都有一段时间在抖动,有时高电平有时低电平。

一种方法是用RS触发器

因为抖动只是小范围的抖动,只要一次R或S有效之后,就已经把状态保存了,就算抖动也不会改变
第二种方法是延时再去判断,延时10-20ms再看,这个是通过软件实现,RS触发器是硬件的方式。抖动在10ms以内结束,而人的动作速度最快在百毫秒级别,所以这是可行的
对于按键长按,有两种解读方式,一种是认为按下只视为一次操作,只会输出一次,另一种是发现你一直按着的时候就以一个固定频率输出这个按键对应的值。
查询法
独立式

注意由于P1到P3有内置上拉电阻所以可以不外接上拉电阻,但是P0口需要。然后根据这样接,按下按键那么读到就是0,如果没按就读到1

P0F,P1F等是跳转到按键对应的程序
矩阵式

其中行用了三个P口,列用了三个P口,于是就可以只用6个口就能获取9个按键的信息
为了检查是哪个键被按下,有行扫描法和线反转法两种方法,依次介绍
行扫描法:先让行的P口作为输出,全部输出0,此时读取列的数值,如果键全部断开,则会读到全1,非全1就说明有键被按下,这里举键4被按下为例,在列的P口就会读出101。那么根据这个就能锁定被按下的键在第一列,接下来让行的P口依次输出011,101,110,看第一列的读数。当行输入011,第一列读到1,当行输入101,第一列读到0,当行输入110,第一列读到1,于是就可以确定是第一行的被按下了,于是就确定是第一行第一列的键4被按下。
线反转法:先让行的P口作为输出,全部输出0,如果不是全1就说明有键被按下,也是举键4被按下为例。接下来让列的P口作为输出,全部输出0,读取行的数值,值为0的那个就是被按下的,这里第一行会显示为0,于是就确定是第一行第一列
显示器¶
LED有两种接法

想要控制亮暗,可以通过调节占空比来实现
有静态和动态的两种显示方式,静态就是显示字符的时候亮就一直亮,没有周期变化,动态就是字符显示的时候有亮的时候也有不亮的时候
静态的

最简单的连接方式就是这样,其实也就是将D/A换成了LED

动态的

这样就节省了I/O口,而且现在LED的公共端不是接VCC或接地了,而是接一个片选信号,比如说原本应当是共阳极的,那我只让D7为高电平,其余都是低电平,那此时其他的就相当于都不亮,只控制D7选到的那个亮。不过这样对于片选信号的驱动能力要求较高,可以用三极管来进行电流增大。这样动态接法的限制在于亮度就不够高,它让D7亮完就D6亮这样一路下去,要保证人眼看不出的话那切换速度就要很快,每一个LED显示的时间其实很短,就导致亮度会比较小。如果对于那种滚动LED,缩短曝光时间,拍出来的字就是断开的
系统设计案例¶





这里P0如果不是数据地址复用的那种,只是做通用IO的话需要上拉电阻





