跳转至

硬件软件基础

写在前面

本笔记基于田翔老师课件,田翔老师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

image.png

数据总线在P0,8位。P0为什么可以做地址总线也可以做数据总线呢,因为不同的时间内它充当的功能不一样。在这里373是一个锁存器,当P0给出地址信号的时候就记录下来锁存,然后之后P0就输出数据了。锁存用的控制信号是ALE(address latch enable),中文是地址锁存信号。锁存器就是异步触发器,没有时钟,不是边沿触发而是电平触发。

锁存器和寄存器的区别,其实它们都是触发器,用来保存数据的,只是锁存器用来保存的是一个临时数据,后续可能会传其他的,而寄存器保存的是和状态相关的配置信息。

ALE还可以以时钟⅙的频率输出方波,所以它还能用来当时钟。ALE在一个机器周期里会有两次变成高电平又变回低电平,每次它变成高电平的时候向ROM里取一次数据,所以一个机器周期取两次数据,最多取16位

P0口除了做I/O口以外可以做地址总线的低八位,和数据总线的八位(总共八位)

image.png

读操作就是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是十六进制

image.png

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

复位后状态

image.png

除了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指令。

MOV P0, #0FFH

这是51的汇编语言,MOV是操作码,表示数据传送,语法是MOV [目的操作数] , [源操作数]。此处目的操作数是P0,也就是51的P0 I/O口,而#0FFH是源操作数。#是51的立即数前缀,也就是把0FFH当作直接使用的常数,0和H是16进制数的前缀和后缀(0可加可不加),FF换算成二进制就是11111111。因此这条代码的意思就是让P0为全1

MOV P0, #0FFH
MOV A, P0

执行上面这两条语句,你无法直接得知A里面是什么值,需要结合P0的外部电路来获得

而如果执行下面这两条语句,可以肯定A里面就是全0

MOV P0, #0
MOV A, P0

注意用P口做输入的时候,需要先把P口置全高电平,确保接地的三极管截止,否则无法接受输入的信号

STM32和ARM比51更加复杂,对于初学者来说不合适,因此这个课讲51要理解这些基本的东西

数据指针DPTR(data pointer)

对8031,要注意复位的时候外部ROM一定要有0000H这个地址,不然它复位完PC回到0000H没法运行。

很多时候卡bug是因为PC取了错的指令,导致没法运行下去,因此用一下复位回到原来的状态就可以了

P口

无论是哪个P口,要从它那里读取东西(输入)之前都必须写FFH,保险起见必须这么做

P是port的缩写

image.png

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 P0, A;

ANL是and logic,这个指令对于P0和A按位与然后再把结果写进P0,这个操作读的就不是引脚而是读锁存器

只有当目的操作数是P0而且需要用到它原来的状态的时候才会用读锁存器,像是MOV P0, #0这样就是直接把0写进去不会用到P0原来的值所以就也不会有读锁存器的情况

image.png

这些是所有读-修改-写功能的指令,都是读latch里的值而不是读端口上的值,是为了避免误读,因为有些外部连接会导致你写出去的值和你之后读回来的值不一样。

8031不能把P0当作I/O口是因为它必须用外部的ROM,它的P0就只能用来做地址和数据总线了

P1是纯I/O口,比P0简单

image.png

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

image.png

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

image.png

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

image.png

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

image.png

用P0做通用I/O的时候要配上拉电阻,但P0做地址数据总线复用的时候不用上拉,它内部的那个上拉会被启用

注意-WR和-RD是通过P3口控制的,用作外部扩展RAM,而外部扩展ROM是用-PSEN控制的

课本上说控制总线12根,把P3口和那四根给算上,如果考试问你就说这样是对的就行

image.png

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做数据地址总线复用的时候,不需要外接上拉电阻。

image.png

外部扩展的时候不要往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这么算。

image.png

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

image.png

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

image.png

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

image.png

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

image.png

我才知道原来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做保护。

展开看位寻址区

image.png

注意,直接寻址和位寻址的20H是不一样的,直接寻址比如说MOV A, 20H,找的是图中位寻址区07到00这八位。而MOV C, 20H,找的是24H那八位中标号为20的那个bit

SP在复位后会自动跑到07H,但这是01对应的R0到R7,不太推荐(因为那是1区的位置,如果你要用其他区的R0到R7那就把SP放到上面去),一般会推荐放到数据缓冲区,就是位寻址区上面的部分,一般来说调到60H即可

演示一下位寻址的写法

SETB 00H ; 方法1:直接位地址访问,置位20H.0(位地址00H) 
CLR 3AH ; 置0 27H.2(位地址3AH = 27H*8 + 2) 
MOV C, 2FH.7 ; 方法3:字节地址+位序号,将2FH的第7位送入进位位C 
MOV 20H.1, C ; 将进位位C的值送入20H.1

SETB C ; 方法2:位名称访问,置位PSW.7(进位标志C) 
CLR RS0 ; 置0 PSW.3(工作寄存器组选择位RS0)
MOV C, 0D0H.5 ; 方法3:字节地址+位序号,读取PSW.5(F0位)到C 
MOV 0D0H.4, C ; 将C的值写入PSW.4(RS1位)

SETB P1.0 ; 置位P1口第0位(输出高电平)
CLR 90H.7 ; 方法1:位地址97H=90H+7,置0 P1.7 
MOV C, P1.3 ; 读取P1.3的电平到C 
MOV P3.2, C ; 将C的值输出到P3.2

SFR

image.png

只有最左边那一列的是可以位寻址的,像是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

; 初始化栈指针SP为0x6F,栈顶定位内部RAM 0x6F,后续压栈操作从0x70地址开始 
MOV SP, #6FH
; 给工作寄存器R0直接赋值0x30,将R0作为地址指针(操作R0本身,无@),指向内部RAM 0x30单元 
MOV R0, #30H 
; 将立即数0x85送入累加器A,作为本次待处理的原始数据(二进制1000 0101)
MOV A, #85H 
; 压栈保存A的原始值0x85:SP先自增1至0x70,再将A存入0x70单元,为后续恢复A做准备,此时SP=0x70 
PUSH ACC
; 交换累加器A的高4位和低4位,0x85(1000 0101)交换后变为0x58(0101 1000)
SWAP A 
; 将A与立即数0x0F做与运算,屏蔽高4位仅保留低4位,提取原数0x85的高4位0x08,结果存回A中 
ANL A, #0FH
; 长调用ASCII子程序:1.跳转子程序将A中0x08转为对应数字ASCII码0x38(执行ADD A,#30H)
; 2.指令自动压入16位返回地址(下一条指令MOV @R0,A的地址),遵循低8位先压、高8位后压规则
; 3.压栈过程SP连续自增2次,从0x70→0x71→0x72,堆栈新增返回地址高低8位
; 4.ASCII子程序末尾通过RET指令弹栈(高8位先弹、低8位后弹),SP自减2回0x70,恢复调用前堆栈状态 
LCALL ASCII 
; 间接寻址:将A中转换后的ASCII码0x38,存入R0指向的内部RAM 0x30单元(@表示操作R0指向的地址,非R0本身)
MOV @R0, A
; 弹栈恢复A的原始值:从栈顶0x70单元取出之前保存的0x85送入A,SP自减1回初始的0x6F,还原累加器初始状态
POP ACC

旧版的田翔老师课件上会出现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 [某值]

时序

image.png

51的主频最高是12M,由此可以得到一个机器周期最短是一个微秒,最简单的指令也得要一个机器周期,四个机器周期的指令只有乘除法,没有三个机器周期的指令。很多manual里一般就把1微秒作为周期的常数了。

待机是通过停止时钟来完成的,大部分功耗都是开关导致的动态功耗,而静态功耗是很低的

image.png

一个机器周期分为六个S,每个S里又有P1和P2(P1和P2各是一个晶振周期)。在一个机器周期里会对ROM读两次,是为了读那个指令。就算只需要读一次它也会读两次,第二次会被忽略

所以三字节指令一定是两周期。因为已知指令只有1周期,2周期,4周期。而4周期指令只有MUL AB和DIV AB,这两个是单字节的。而一个周期只能从ROM取两个字节,那三字节指令肯定取不完,所以需要两个周期。

image.png

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两次高电平隔了一个周期,而不是像之前那样保持一个周期里面有两个

image.png

image.png

软件部分

所有硬件能支撑的指令的集合称为指令集,通常硬件越复杂,指令集越大,那么你软件方面就越简单,而如果硬件简单,那么指令集就小,你要执行相同难度的任务你要写的软件代码就越麻烦

计算机只认机器码,因为难背所以用了助记符,助记符和特定功能的机器码一一对应

汇编语言的格式

[标号: ] 操作码 [目的操作数], [源操作数]; [注释]

标号可不写,注释写在;后面

标号的作用是相当于另外起了名字,比如LCALL ASCII,其实是有ASCII: ADD A, #30H,那ASCII相当于是这段代码的别名

从功能上分指令,有数据传送类,数据处理类(运算),控制转移类

控制转移类指令就是像是循环那种,它可以隐性改变PC值

image.png

我来解释这个程序。这里是被写好的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位的

image.png

image.png

寻址方式

==立即寻址==是从ROM里寻址的方式,比如立即数就是这样,立即寻址的代码至少两字节,因为它要从ROM里找数

MOV DPTR #8000H是三个字节的,第一个字节是告诉你要往DPTR里要放一个数这个事情,然后这个立即数作为地址是十六位的,需要两个字节来表示,所以总共是三个字节。另外这个代码是双周期的,要知道代码只有1或2或4个机器周期的,4周期的只有乘除法,这个不是乘除法所以排除。而一个周期处理不完16字节的,所以就需要两个周期

==直接寻址==是在片内RAM或SFR里寻址的方式,操作数不用加#。直接寻址是访问大多数SFR的唯一方式(除了A, B, DPTR这些通过寄存器寻址也能直接寻址的寄存器)

image.png

光写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 R0, #80H
MOV @R0, #0FFH

MOV 80H, #0FFH会把数据存入SFR的80H(P0),没法存进高八位RAM里,只有用@R0这种方式才能访问52的高八位RAM

用@DPTR访问的一定是外部存储器,访问方式比如说MOVX A, @DPTR; 这里需要DPTR里已经写好了要访问的地址

MOV @R0, A是单字节,MOVX A, @DPTR也是单字节。这里我要重新说明这个字节数是存在ROM里的指令的长度,什么情况下需要更多的字节呢,就是比如说直接存了八进制数在程序里面,或者直接存了地址在程序里面才需要,但是这里只是涉及到取用的操作,不需要你把DPTR的具体内容存到程序里面,所以是单字节的。

image.png

你看这样才是需要双字节的,因为它把这个数直接存到程序里了。除了存数,还可以存地址,比如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

image.png

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就行。

image.png

rel取值范围-128到+127,Byte

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

image.png

注意这里是十六进制,10H对应16,所以并不会发生30H和40H之间有重叠的情况

ORG 2000H是伪代码,只是告诉你代码从2000H开始,不是你会用到的汇编语言

以上程序中的LOOP相当于

1
2
3
4
5
6
for(i=16; i>0; i--)
{
    *b=*a;
    a++;
    b++;
}

其中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,后面的不能用

对于这种往前跳的偏移量,公式是一样的,不过现在要用补码的形式表示,这应该是少有的要用到补码的地方

image.png

补码是反码加1,FFH(16进制的全1)减去那个就是反码了,后面还加了1

RAM内部还有一块位寻址区,就是用来做==位寻址==的,除了那一块之外,还有SFR的这些也是可以位寻址的

image.png

位寻址和直接寻址的样子一样,比如说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.0CLR 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.1CLR PSW.7 PSW.7 就是 C(进位位)
ACC 0E0H 累加器 CPL ACC.0MOV 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

image.png

别忘了位寻址

这个图里的CY并不会在代码里出现,代码里只有C,所以含C的,这个C用到的是寄存器寻址而不是位寻址?C是位寻址吗?

寄存器寻址和寄存器间接寻址都是不需要多一个字节的

1个机器周期会在ROM里取两个字节,所以双字节单周期是可以的

三字节指令至少要两个周期,非乘除法的一定是两个周期

image.png

外部扩展RAM无法直接寻址,需要配置Ri和P2来分别制定低八位和高八位

任何型号,无论31,51,52,都是有片内RAM的,所以没有8031用不了A,DPTR之类的问题

image.png

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

image.png

这里的INDEXED ADDRESSING在中文介绍里区分成了基址寄存器加变址寄存器寻址和相对寻址

指令相关

不允许MOV R0, R1, MOV R0, @R1等

MOV P1, P3是可以的,这两个都是直接寻址

注意MOV Rn,direct是两个周期的,MOV Rn, #data是一个周期的

比较特殊的是MOV DPTR, #data16,用十六位立即数对DPTR赋值,是三字节指令,两周期

image.png

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有更特殊的规定

image.png

所以POP SP的功能就是把SP当前指向的内存里放的值赋给SP,之后不再-1

这条指令的意义在于如果有不同的堆栈那可以通过这种方式切换,但是注意这是非常规的,非常少用,只要知道它执行之后发生的事即可

image.png

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

image.png

DPH和DPL分开存进去,注意POP的时候和PUSH的时候对称的,反着来的,先进后出。

在改变A的数据时,注意P,就是PSW里的那个奇偶校验位也会变化

进行算术运算的时候用到ALU,结果一定放在A里面,一定是ADD A, 某某的形式

image.png

注意CY和OV各论各的。CY是当成八位无符号数来看,如果有进位就为1,而OV是当成有符号数来看,如果两个正数相加得到负数,比如例二的情况,那它就变成1了,而例一是两个负数相加得到正数,所以OV也是1。

AC是半进位,P是奇偶校验位

ADDC是带进位的加法,应用场景就是低八位加完可能有进位,那再进行高八位加的时候就要加上这个进位的1

减法只有一种形式,没有不带借位的减法,所以你如果要做减法然后不确定借位有没有的话就必须先做一个CLR C

image.png

image.png

务必注意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,就得自己写

image.png

先把C清零,然后对低八位减一放回去。然后注意还要对高八位做一次SUBB,因为对低八位做减法的时候可能会使得C=1,那你就要再对高八位做带借位减法,然后再把高八位放回去。对所有十六进制数的加减都得这么搞

注意ADD和SUBB只能以A为目的操作数

想要把加法结果用十进制表示的话,就在加法之后加一个DA A的指令就行。这个调整只对加法有效,对减乘除不行

image.png

DA A得到的还是BCD码

image.png

image.png

就是低位和低位加,高位和高位加,中间需要调整到十进制再放回去。这里为什么不用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中有体现。

image.png

这里也是一样的,这就体现出用指针的好处了,就可以写循环了。第一次循环的时候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

image.png

CPL A; 是对A取反,是converse position logic的缩写

注意只有CLR A这一条指令是对整个字节清零的CLR指令,其余CLR都是对位操作。类似的,也只有CPL A这一条是对整个字节按位取反,其余的都是对位取反。

ANL是and logic,ORL是or logic,XRL是exclusive or logic异或

这些指令都可以按这样写

image.png

后面两个数字,第一个数是字节数,第二个数是周期数

ANL用来保留和屏蔽字节中的某些位,想要保留的就和1与,想要屏蔽的就和0与

ORL用来保留和置1,想要保留的就和0或,想要置1的就和1或

XRL用来保留或取反,想要保留的用0异或,想要取反的用1异或

image.png

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原来指向的位置的基础上偏移这么多。

接下来是有条件的跳转

image.png

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

image.png

LCALL是long call,三个字节,因为存十六位地址需要两个字节。然后呢它做的事情就是把地址存进堆栈然后跳转,然后遇到RET的时候再从堆栈把地址取回来然后就可以返回了。ACALL和AJMP类似,也是用两个字节就完成了。RETI是return interupt,做的事情和RET差不多但是不能混用,RETI只有在中断相关的情况才能用,如果是LCALL或ACALL是只能用RET的

还有一个很特殊的空指令NOP; 连操作数都没有,就这么一个操作码,就让PC+1,什么都没干。这条代码存在的意义上它可以延后一个机器周期,在一些时间需要严格控制的场景可能有用

image.png

image.png

注意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, 30H
ANL P1, #0FH
CLR A
CPL A
位操作 MOV C, 30H
ANL C, P1.0
CLR P1.0
SETB C
CPL P1.0

image.png

注意JBC的C不是CY而是clear,所以这条指令在跳转之外还会把这一位置零

保存在内部的东西都可以直接自增自减,无论是通过间接寻址还是基变还是寄存器寻址

注意到这几个算数指令,只有MUL和DIV是4个机器周期,只有DPTR自增是2个周期,其余都是1个周期

image.png

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

image.png

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

image.png

image.png

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

image.png

访问一个字节里的一个位用像是P1.0,PSW.7这样的方式来确定这一位

所有指令及其字节数和周期

image.png

image.png

image.png

image.png

image.png

image.png

image.png

伪指令

image.png

ORG是告诉烧录代码的机器这段代码是从哪里开始录入

DB是define byte,用来给ROM存表格的,这里TAB就是table

49H就是十进制73转16进制的

image.png

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

image.png

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

image.png

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

image.png

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

程序

顺序

image.png

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

另一种方法

image.png

注意XCHD只有这种写法,因此必须用R0做指针。但是其实没有必要用这种复杂指令,多用几个MOV避免非法指令没什么问题。

其实甚至可以将原数和#10H除,商就是高位BCD码,余数就是低位BCD码,由于这里涉及到十进制和十六进制,其实并不是常规意义上的数制转换,而是一种根据它的结构的取巧的计算方法。

image.png

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

我才发现原来不写注释是不需要加;的

image.png

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

其实没有十进制,它里面只有二进制。DA A实现的十进制实际上是把十六进制的换成十进制的样子,但是本质还是一个十六进制数。

image.png

image.png

条件

用C做条件判断

image.png

其实判断是否等于0用JZ和JNZ就行

考试时建议画流程图,而且是一看就知道代码怎么写的流程图,书本上的流程图其实还是不够清晰。

image.png

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

image.png

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

image.png

对于这个题目我自己写的程序是这样的,我认为我这样写是最清晰的,PPT上给的那个有些莫名其妙

    CLR C
    MOV A, 30H
    CJNE A, #0BH, COM11
COM11: JNC GE11; 如果大于等于11(也就是大于10)跳转GE11(Greater or Equal11)
    CJNE A, #05H, COM5
COM5: JC L5; 如果小于5跳转L5(Less than 5)
    MOV 31H, #0
    SJMP FINISH
GE11: INC A
    MOV 31H, A
    SJMP FINISH
L5: DEC A
    MOV 31H, A
FINISH: SJMP $
    END

我在写的过程中发现,如何安排相应条件的代码的位置是很重要的,我觉得这样写清晰的原因是,对于特殊的条件,比如大于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路分支的跳转已经有固定代码了

image.png

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

image.png

这部分相当麻烦,你需要区分开各种读取数据的方式的范围是多少,这里我进行一下总结

指令类别 适用指令示例 寻址范围
内部数据RAM(MOV MOV A, 70H
MOV 60H, #data
内部RAM低128字节:00H–7FH
SFR(特殊功能寄存器):80H–FFH
内部数据RAM(MOV MOV A, @R0
MOV @R1, #data
内部RAM低128字节:00H–7FH
内部RAM高128字节(若有):80H–FFH
内部数据RAM(MOV MOV A, R3
MOV R7, #data
当前选中的工作寄存器组(R0–R7),位于内部RAM低32字节的某一组(00H–1FH中的8个位置)
MOV A, #25H 无地址(数据来自指令操作数)
外部数据RAM(MOVX MOVX A, @DPTR
MOVX @DPTR, A
外部数据RAM全部64KB:0000H–FFFFH
外部数据RAM(MOVX MOVX A, @R0
MOVX @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, 07H
SETB 20H.0
CLR P1.0
内部RAM位寻址区:20H–2FH的128个位(位地址00H–7FH
SFR中可位寻址的位(位地址80H–FFH中的部分)
程序转移 SJMP rel
JZ rel
当前PC(指令首地址+2)偏移-128 ~ +127字节范围内的程序存储器地址

image.png

image.png

R3能装0到255的,但是由于AJMP每个是两字节,@A+DPTR只能在DPTR的基础上找往下0到255字节的,所以只能覆盖128个AJMP,所以这里R3的取值是从00H到7FH,只有128个

这里RL A是左移A,不过这种方法其实也不是完全保险,需要最高位是0才能正确实现乘2,更保险的写法是CLR C然后RLC A

循环

对于循环来说赋初值很重要,不能在没有赋初值的情况下跳到一个循环的内部

image.png

这题我自己写的是这样的

MOV R0, #30H; pointer
MOV R1, #20H; counter
MOV A, #0; 低八位
CLR C; 判断进位
MOV 51H, #0; 高八位

LOOP:
ADD A, @R0; 注意不是ADDC,A只记录低八位
INC R0; 指向下一个单元
JC HIGH; 如果有进位就去高位加1
DJNZ R1, LOOP; 注意DJNZ已经有了R1-1功能,不需要在上面写DEC R1
SJMP FINISH

HIGH:
INC 51H; 高位加1
DJNZ R1, LOOP; 一样效果

FINISH:
MOV 50H, A; 低八位搬到50H
SJMP $
END

我第一次写的时候多写了DEC R1,后面才发现原来DJNZ已经会减一了。

不过还可以优化,这里写了SJMP,但其实可以不需要的,比如DJNZ的那条程序出现了两次,那就可以把它调整到在执行下来的时候本来就会执行,为此可以把JC改成JNC,把操作更多的那种情况(此处指C=1时需要多执行INC 51H)留在“主路”,跳转的功能用来跳过那些不需要做的操作。

MOV R0, #30H; pointer
MOV R1, #20H; counter
MOV A, #0; 低八位
CLR C; 判断进位
MOV 51H, #0; 高八位

LOOP:
ADD A, @R0; 注意不是ADDC,A只记录低八位
JNC NC; 如果没进位就跳过高位加1
INC 51H

NC:
INC R0; 指向下一个单元
DJNZ R1, LOOP; counter-1判断循环

MOV 50H, A; 低八位搬到50H
SJMP $
END

这样就更加合理,另外这种分区写代码的方式也可以从C沿用过来。

image.png

ADD那条其实也算是循环内一定要做的操作,不过它能够产生是否跳转的判据C,所以必须把它放在跳转语句JNC的前面。而其他那些不影响的,比如INC R0,无论放在JNC之前还是NC后都可以,因为它一定会被执行。多出来的操作放在JNC和NC之间。

注意DJNZ实现了C里for循环的i--和i>0,不过它通常放在循环的最后,这样你的counter就直接写循环执行的次数就行。

image.png

这个例子和刚刚写的那个又不同,这里是各做各的,最后用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个机器周期

image.png

其实就是R6从98减到0之后R7才减一次,然后又把R6赋值到98

冒泡排序,如果能看得懂这个那看代码应该没什么问题

DPL是地址,现在通过两层循环来实现从大到小的排序。而且增加了快速结束的条件,就是如果进行完第一轮从来没有交换过,那就直接退出。

每次进行完一次循环,R7会减一,而赋值给R6也会减1。假设R7一开始设为x,那么R7这一层最外层的会运行x次,而R6每次会运行R7次,是随着R7减少也随着减少的。总共要排x+1个数。

image.png

查表

image.png

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

image.png

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

image.png

image.png

这个程序看起来很麻烦,第一次看你可能不明白为什么要定义A然后又要用DPTR,但这其实都是因为汇编无法像C那样直接把变量存下来导致的。比如MOVC A, @A+PC注定了这事只能由A来干。在汇编里寄存器比如A,就是变量,你不能自己随便定义变量,只能在R0到R7以及A, B, DPTR里选,一些特殊操作又只能用A和DPTR,所以就会出现这种需要暂存到其他地方的现象。

如果写成C其实很简单的事情

int zhang = 0x30;
int* address = TABLE; // 其中TABLE是已知的数据表格起始地址
int flag = 0;

for(int i = 200; i>0; i--)
{
    if(*address == zhang)
    {
        flag = 1;
        break;
    }
    address++;
}

if(!flag)
{
    address=0;
}
这里定义的变量明显比汇编的少,只用到了zhang, address, flag, i这四个,结合这部分代码看汇编就能体会到汇编相较于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结尾

image.png

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

image.png

这里注意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的条件语句。注意这里你就直接把它分成不同区块的代码,没有必要为了让代码尽量少而利用它顺序执行的特征来简化代码,先熟练掌握这个写法。

; 输入: A
; 输出: 无,但根据区间分别跳转到 label_le10, label_mid, label_ge20
CHECK_RANGE:
    CJNE    A, #11, check1     ; 比较 A 与 11, 不等则跳 check1(其实这里不用CJNE,用SUBB然后根据C来跳也行)
    SJMP    label_mid          ; A == 11 属于第二区间(10~20)
check1:
    JC      label_le10         ; 若 A < 11 (即 ≤10) 则跳区间1
    ; 到这里说明 A >= 11
    CJNE    A, #20, check2
    SJMP    label_mid          ; A == 20 属于第二区间(10~20)
check2:
    JC      label_mid          ; 若 A < 20 (即 11~19) 则区间2
    SJMP    label_ge20         ; 否则 A ≥ 21 区间3

label_le10:
    ; 处理 A ≤ 10 的代码
    ; ...
    RET

label_mid:
    ; 处理 10 < A < 20 的代码
    ; ...
    RET

label_ge20:
    ; 处理 A ≥ 20 的代码
    ; ...
    RET

案例2:数组求和,从30H开始放16个数,用到循环和16位加法操作,也算是比较经典了

    MOV     R0, #30H       ; 指针
    MOV     R7, #10        ; 计数器,循环 10 次
    CLR     A              
    MOV     R2, #0        
    MOV     R3, #0
sum_loop:
    MOV     A, @R0         ; 取一个数
    ADD     A, R3          ; 加到低位,第一次A是0所以看起来没区别,但后面就正常加了
    MOV     R3, A
    CLR     A              ; 处理进位
    ADDC    A, R2
    MOV     R2, A
    INC     R0             ; 指针后移
    DJNZ    R7, sum_loop   ; 减一,非零则继续循环
    ; 结果在 R2:R3

案例3:延时程序,熟练掌握写法,注意DJNZ放在最后面,而且有两层

1
2
3
4
5
6
7
8
9
; R5:R4 为计数 (比如 1000)
DELAY_MS:
    MOV     R5, #40        ; 外层
DELAY_OUTER:
    MOV     R4, #250       ; 内层 (250*2=500µs)
DELAY_INNER:
    DJNZ    R4, DELAY_INNER
    DJNZ    R5, DELAY_OUTER
    RET

案例4:查表找值,用好MOVC和DPTR

; 定义表格(放在 ROM 中任意位置)
    ORG     1000H
MY_TABLE:
    DB      12H, 34H, 55H, 78H, 0ABH
    DB      0CCH, 0DDH, 0EEH, 0FFH, 11H
    DB      22H, 33H, 44H, 66H, 77H
    DB      88H, 99H, 0AAH, 0BBH, 0CCH
TBL_LEN  EQU   20                ; 表格长度

; 查找程序
; 入口:无
; 出口:DPTR = 匹配项地址(ROM 地址); 若未找到则 DPTR = 0
SEARCH_ROM_TABLE:
    MOV     DPTR, #MY_TABLE      ; DPTR → 表格开头
    MOV     R7, #TBL_LEN         ; 剩余查找个数
NEXT_BYTE:
    CLR     A
    MOVC    A, @A+DPTR           ; 读当前字节
    CJNE    A, #55H, SKIP        ; 不等于 55H 则跳转
    ; 找到匹配,此时 DPTR 已经指向匹配项的地址
    RET                          ; 直接返回,DPTR 就是所需地址
SKIP:
    INC     DPTR                 ; 指向下一个字节
    DJNZ    R7, NEXT_BYTE        ; 未查完继续
    ; 全部查完未找到
    MOV     DPTR, #0
    RET

系统扩展

ROM和RAM扩展

这张图是ROM扩展的时序

image.png

一个机器周期包含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扩展的写的时序

image.png

image.png

image.png

这个是和ALE配合的锁存器,-OE是output enable,当它为高电平的时候处于高阻态Z。当-OE为低电平的时候是允许输出的,此时看LE即latch enable,当LE为高电平,Q原样地获取D,当LE为低电平,Q状态不变,实现锁存

ALE信号接LE,当ALE高电平,P0输出地址低八位,373获得,随后ALE低电平,373把地址低八位锁存,P0再去输出数据

image.png

型号27后面的数字除以8就是容量,比如16对应2K,32对应4K

其中chip select是最麻烦的,要用它来确定你能扩展的64K空间里,占的究竟是哪些空间。因为P0和P2总共提供十六根地址线,可以扩展64K,但是比如说你扩展的ROM只有2K,那这2K放在64K中的哪里,就要由CS定。其余的都是无脑接的,数据线接数据线,地址线接地址线,-OE接-PSEN

RAM也是类似的

image.png

-OE和-RD接,-WE和-WR接

注意8031外部扩展ROM的时候注意要有0000H

如果扩展多个ROM或RAM,注意分配的地址不能重叠

ROM扩展

image.png

image.png

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

image.png

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

image.png

每个ROM是8K

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

image.png

这个也好理解,就是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对齐的话,那它就不连续,需要很多跳转,就很麻烦

image.png

线译码就是把译码器给省了,就单独控制,这样硬件简单,但是软件方面就比较麻烦

高三位011唯一对应U1,101唯一对应U2,110唯一对应U3,其余的都会出现两个都访问的情况就很乱套,比如000那就全都访问了

所以线译码写软件就要比较小心,只能用那些能区分开的地址,不然就乱了

总结一下全译码就是没有并联的都接到译码器了,部分译码就是只用了够用的线接到译码器,线译码就是没有译码器

image.png

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

image.png

RAM扩展

image.png

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

image.png

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

image.png

线译码由于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的输入和输出

输出扩展

image.png

image.png

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

输入扩展

image.png

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

image.png

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

image.png

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

image.png

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

image.png

我想的另一个验证方法是ADD A, #1然后根据C来判断,不过可能就没这里这么简洁

这样操作是比较低效的,因为是在一直查询,如果能改成中断的话,就不用占用主程序了,还可以运转其他的操作。

总线扩展的结构都是这样的

image.png

比如CPU是8051,可编程I/O接口是LS273,I/O设备是DAC。当然如果单片机本身已经够用了,就没必要外接别的东西了

外部扩展芯片的时候,芯片上有CLK,如果没有特殊要求接ALE即可

然后其实其他的芯片,告诉你到底什么情况下是输入输出,以及怎么控制它的状态,你就按要求去写软件就行了,其实这个是简单的

本页总阅读量