硬件软件基础¶
写在前面¶
本笔记基于田翔老师课件,田翔老师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即可
然后其实其他的芯片,告诉你到底什么情况下是输入输出,以及怎么控制它的状态,你就按要求去写软件就行了,其实这个是简单的
本页总阅读量次