跳转至

中断、数模转换、键盘、系统设计

中断

子程序的调用是有计划地被打断,显式调用。

中断是不可预见程序执行到何处发生的,但是可以提前写好中断的子程序,这样如果发生中断就可以先去处理了再回来继续执行

与中断相对的是查询(或称轮询,polling),就是你时不时检查一下有没有出问题,这样比较耗运算资源,而中断的反应是即时的,而且引发中断的事情没有发生的话,是不干扰程序运行的

中断源是引起中断的原因,比如按键,定时器,断电保护等

开中断时CPU才能响应中断,相对地关中断就把中断给屏蔽了不去处理

中断有优先级,高级的中断可以打断低级的中断

image.png

image.png

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,中断服务程序

中断控制示意图

image.png

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都是特殊功能寄存器用来锁存中断请求标志

image.png

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

中断的实际处理流程

image.png

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

image.png

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

然后PPT上是这样写

image.png

image.png

再补充一个点,哪些中断标志位要手动清零。

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

image.png

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

中断的锁存器和控制

TCON的组成

image.png

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

image.png

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

image.png

image.png

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

image.png

如果主程序执行的时候已经在执行了,必须等这一条指令执行完才去响应中断

若执行指令是RETI或是访问IE或IP的时候,执行完该指令至少再执行一个指令才响应中断,所以如果你一直在对IE或IP操作,那它也无法响应中断

响应中断的时候会置位优先级触发器,自动清零TCON的那些,但是不会清零SCON的TI和RI。另外响应中断也会保护断点,把PC弄进堆栈后面恢复

RET只会做PC恢复的那一步,RETI会完成中断相关触发器的处理,所以不能用RET替换RETI。RETI多做的是清除中断优先级触发器,这个触发器是用来封锁更低优先级的中断请求的,经过RETI就可以把这个给解除,如果用的是RET而不是RETI,那这个触发器就一直封锁着,没法处理低优先级的中断。然后RETI还会告知中断已经处理完毕,而RET不会,就会导致系统误认为还在处理中断

image.png

所以中断子程序必须用RETI而不是RET,不然它会认为一直还在执行之前的那个中断,这样下一次这个中断再次出现它就不会响应了。

五个中断程序的入口地址固定,而且那几个入口之间没有多少空间,所以如果中断程序较长需要先来一个LJMP跳到空间充裕的地方写

中断相关代码

实际放的东西是这样的

image.png

外部中断

image.png

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

image.png

注意外部中断如果是电平触发需要手动清掉中断标志位

定时计数器

用不确定何时发生的外部事件触发,就是计数器,用内部的固定频率的脉冲来触发,就是定时器

image.png

注意外部脉冲触发的原理是在第一个周期的S5P2里采样,是高电平,第二个周期的S5P2里再次采样,得到低电平,就记录为一次,这意味着完成一次计数至少要2个机器周期,所以能计数的最高频率是振荡频率的1/24,如果是1MHz,那外部输入触发的最高就是2μs计一次

GATE是自己配置的,-INT1是外部管脚,设GATE=1相当于屏蔽外部管脚

上面这个图只是一种模式的情况,还有其他模式,具体看设置的相关寄存器

定时计数器TMOD寄存器不能位寻址,一整个字节连在一起,需要全部想好之后把一个字节弄进去做好配置

image.png

GATE是TMOD.7

有两个定时计数器,T1和T0,这个0和1也是对好的,比如1就对应TR1和-INT1,0就是TR0和-INT0

GATE和C/-T已经知道,就看M1和M0

image.png

13位计数器和老版本兼容有关,自动装载的8位计数器是指计完一轮它可以从头开始循环计数,而16位的那个就不行你得重新配。而方式3注意用不了T1

可重装载指的是8位的TL0计数到全1之后就把TH0的值复制到TL0继续计数到全1,这样循环,这个过程不改变TH0的值,这样就意味着你可以做一个低于256的一个循环计数

image.png

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

image.png

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

image.png

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

; ============================================
; 8051定时器方式3例程(汇编)
; 功能:TL0和TH0独立定时,翻转P1.0/P1.1
; 晶振:12 MHz
; ============================================

                ORG     0000H
                LJMP    MAIN

; ---------- 中断向量 ----------
                ORG     000BH          ; TL0 中断入口
                LJMP    TL0_ISR

                ORG     001BH          ; TH0 中断入口(借用定时器1向量)
                LJMP    TH0_ISR

; ---------- 主程序 ----------
                ORG     0030H
MAIN:
                ; 设置定时器0为方式3(TMOD低4位 = 0011B)
                MOV     TMOD, #03H

                ; 装载初值(8位)
                MOV     TL0, #9CH      ; 256 - 100 = 156 (0x9C)
                MOV     TH0, #38H      ; 256 - 200 = 56  (0x38)

                ; 允许全局中断及两个定时器中断
                SETB    EA
                SETB    ET0            ; 允许TL0中断
                SETB    ET1            ; 允许TH0中断(实为TF1)

                ; 启动定时器
                SETB    TR0            ; 启动TL0
                SETB    TR1            ; 启动TH0(使用TR1)

                SJMP    $              ; 主循环空转

; ---------- TL0 中断服务 ----------
TL0_ISR:
                MOV     TL0, #9CH      ; 重装初值
                CPL     P1.0           ; 翻转P1.0
                RETI

; ---------- TH0 中断服务 ----------
TH0_ISR:
                MOV     TH0, #38H      ; 重装初值
                CPL     P1.1           ; 翻转P1.1
                RETI

                END

TCON是可位寻址的,前面中断也讲到过

image.png

image.png

注意是没有用TL的高三位,这个配起来比较麻烦可以不用这个模式

方式1就是最开始看的那张图,可以自己配置TH1和TL1作为初值

方式2是下面这样的,前面控制的是一样的,实际计数用的是TL1,8位,0到255,当到达256的时候触发TF1=1,然后把TH1的值弄到TL1作为初值

image.png

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

image.png

52的定时计数器不考

image.png

另外如果是外部扩展的减法计数器,假设也是16位,如果要计数65536,一开始应该写0,这样它会回到65536,然后再减65535次得到0。如果是加法十六位,也是写0

我自己写了一个配定时计数器中断要用到的寄存器汇总

image.png

最上面是说明各个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即可

程序例子

image.png

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

image.png

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

image.png

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

image.png

image.png

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

image.png

最好加个LJMP

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

image.png

image.png

务必注意这里等待的指令是用JB和JNB P3.3(-INT1) $来原地跳转的,这和之前的SJMP $不同,这是有条件的等待

JB P3.3, $的目的是如果一开始是高电平那就先别开始计时,等低电平来了之后再启动TR1,但此时仍未开始计时,直到-INT1变为高电平的时候才开始计时

image.png

这里是演示设置两个中断,一个是外部中断,一个是定时器T0

主程序只写了配寄存器,之后直接SJMP $

image.png

我这里补一个不同时间翻转P1.0的程序的对比(都假设晶振12MHz),最好用的是可重装载的,这样你就不用自己在每个中断触发时再装载,但是可重装载的只能搞256μs,如果要你搞50ms的,那方式1可以提供最多65536μs也就是65ms的计时,用方式1方便。但是如果要你用2s的翻转,那就连方式1都不能支持了,需要用上外部循环。

; ==========================================
; 程序1:200μs 翻转 P1.0
; 定时器0,模式2(8位自动重装)
; 12MHz晶振,机器周期 = 1μs
; ==========================================
        ORG     0000H
        LJMP    MAIN
        ORG     000BH          ; 定时器0中断向量
        CPL     P1.0           ; 翻转P1.0(这里由于语句很少所以直接写到中断入口这里去了,但常规应当LJMP)
        RETI

        ORG     0030H
MAIN:
        MOV     TMOD, #02H     ; 定时器0,模式2(8位自动重装)
        MOV     TH0, #38H      ; 重装载值:256-200=56 (0x38)
        MOV     TL0, #38H      ; 初始值同重装载值
        SETB    ET0            ; 使能定时器0中断
        SETB    EA             ; 总中断允许
        SETB    TR0            ; 启动定时器0

        SJMP    $              ; 主循环等待中断
        END
; ==========================================
; 程序2:50ms 翻转 P1.0
; 定时器0,模式1(16位)
; 12MHz晶振,机器周期 = 1μs
; ==========================================
        ORG     0000H
        LJMP    MAIN
        ORG     000BH          ; 定时器0中断向量(这里也是偷懒了,应该用LJMP的)
        MOV     TH0, #3CH      ; 重装高字节 0x3C
        MOV     TL0, #0B0H     ; 重装低字节 0xB0
        CPL     P1.0           ; 翻转P1.0
        RETI

        ORG     0030H
MAIN:
        MOV     TMOD, #01H     ; 定时器0,模式1(16位)
        MOV     TH0, #3CH      ; 初值高字节
        MOV     TL0, #0B0H     ; 初值低字节
        SETB    ET0            ; 使能定时器0中断
        SETB    EA             ; 总中断允许
        SETB    TR0            ; 启动定时器0

        SJMP    $              ; 主循环等待中断
        END
; ==========================================
; 程序3:2s 翻转 P1.0(中断向量跳转方式)
; 定时器0,模式1(50ms中断) + 软件计数器(计数40次)
; 中断向量处仅放置 LJMP 指令,ISR 单独编写
; 12MHz 晶振,机器周期 = 1μs
; ==========================================

        ORG     0000H
        LJMP    MAIN

        ORG     000BH          ; 定时器0中断向量入口
        LJMP    TIMER0_ISR     ; 跳转至实际中断服务子程序

        ORG     0030H
MAIN:
        MOV     TMOD, #01H     ; 定时器0,模式1(16位)
        MOV     TH0, #3CH      ; 50ms 初值高字节 (0x3C)
        MOV     TL0, #0B0H     ; 50ms 初值低字节 (0xB0)
        MOV     R7, #40        ; 软件计数器 = 40(2s / 50ms)
        SETB    ET0            ; 使能定时器0中断
        SETB    EA             ; 开启总中断
        SETB    TR0            ; 启动定时器0

        SJMP    $              ; 主循环无限等待中断

; ========== 中断服务子程序(独立放置) ==========
        ORG     0100H          ; 置于安全地址,避免与主程序重叠
TIMER0_ISR:
        PUSH    PSW            ; 保护程序状态字(若主程序使用PSW)
        ; 重装定时器初值(50ms)
        MOV     TH0, #3CH
        MOV     TL0, #0B0H
        ; 软件计数器减一
        DJNZ    R7, EXIT_ISR   ; 若未减到0,直接退出
        ; 计数器归零,翻转P1.0并重置计数
        CPL     P1.0
        MOV     R7, #40        ; 重置计数器
EXIT_ISR:
        POP     PSW
        RETI

        END
; ==========================================
; 程序3(轮询版):2s 翻转 P1.0
; 定时器0,模式1(16位),50ms溢出
; 不使用中断,循环查询 TF0 标志
; 软件计数器 R7 = 40,计满翻转
; 12MHz 晶振,机器周期 = 1μs
; ==========================================

        ORG     0000H
        LJMP    MAIN

        ORG     0030H
MAIN:
        ; 初始化定时器0为模式1(16位)
        MOV     TMOD, #01H
        ; 设置50ms初值(65536-50000=15536=0x3CB0)
        MOV     TH0, #3CH
        MOV     TL0, #0B0H
        ; 软件计数器置40
        MOV     R7, #40
        ; 启动定时器0
        SETB    TR0
        ; 清除可能的溢出标志
        CLR     TF0

        ; 主循环:轮询TF0
LOOP:
        JNB     TF0, LOOP      ; 若TF0=0,继续等待;TF0=1则往下执行

        ; ----- 定时器溢出处理 -----
        ; 1. 重装初值(50ms)
        MOV     TH0, #3CH
        MOV     TL0, #0B0H
        ; 2. 清除溢出标志(硬件不会自动清零)
        CLR     TF0
        ; 3. 软件计数器减1
        DJNZ    R7, LOOP       ; 若R7不为0,跳回LOOP继续等待下一次溢出
        ; 4. 若R7为0,则翻转P1.0,重置计数器
        CPL     P1.0
        MOV     R7, #40
        SJMP    LOOP           ; 继续轮询

        END

串口

并行传输的一个问题是,如果要和时钟同步,那就要严格保证传输的路径是一样长的,不然会有延时,导致同一时刻接受到的信号可能不是你原来传的信号,串行传输不会有这样的问题,就一根线。所以长距离传输一般也是用串口

单工指的是只能收或只能发(比如只有人听电台,没有电台听人),双工模式就是收和发都可以。双工又分为全双工和半双工,半双工指的是收的时候不能发,发的时候不能收(比如对讲机,一个讲的时候另一个只能听)。全双工指的是可以同时进行收发,8051的串口是全双工的。全双工指的不是RXD可以做收和发,而是同一时间,RXD和TXD都在工作

串口分同步传输和异步传输,同步传输就是和时钟对齐,51做不到,只介绍异步传输

异步传输通过通用异步收发器UART(universal asynchronous receiver/transmitter)实现

由于异步传输没有时钟,所以需要比较麻烦的规定,首先收发的速率得是一样的,比如都是9600bps(bit per second),然后数据格式是固定的。

image.png

用低电平来标志数据开始,所以在不传数据的时候是高电平

最低有效位LSB全称是least significant bit, 最高有效位MSE是most significant bit。MSE是D7,LSB是D0

第九位可有可无,要事先商量好有没有,如果有的话是什么含义,比如可以设为奇偶校验位或者其他东西

51的串口接起来很简单,就TX接另一台的RX,RX接另一台的TX,然后共地

image.png

image.png

注意地址是共用的,但其实是不同的硬件。当你把A放进99H,它就经过发送缓冲器串行发出去了,当你把99H的数移到A,它是从接收缓冲器取数。接收缓冲器其实有两个但是用的时候不关心,它有两个是为了防止你还没取的时候又接受一个导致覆盖了,有第二个的话它就会存到第二个里面。这种存储方式称为乒乓存储,其实手机里也有,比如系统更新的时候并不会直接把原来的给清掉,而是现在另一块里更新好了检查没问题了才去清除原来的系统,相当于做了一个备份,包括手机恢复也有用到这样的原理

你自己写不需要用99H,直接用SBUF就行。

然后一个误解是你软件在写的时候一个时候只能执行MOV A, SBUF或者MOV SBUF, A,这不是没法用到全双工吗。但是事实上你执行完一条立刻执行第二条,你发的时候其实还没发完,但这并不会报错,因为发和收是两个东西,他们可以并行工作。不过一般不会这样写,如果用轮询的话应该写成下面这样

1
2
3
4
5
6
7
8
9
; 接收:
JNB RI, $      ; 等待接收完成
MOV A, SBUF    ; 读走数据
CLR RI

; 发送:
JNB TI, $      ; 等待上一次发送完成
MOV SBUF, A    ; 写入新数据
CLR TI

中断的时候讲到过TI和RI,由于串口中断来的时候不知道是T还是R导致的,所以需要你自己去查一下是哪个,然后手动清零,注意中断里TI和RI是不会自动清零的

image.png

注意SM0是SCON.7,RI是SCON.0

方式0通常不用,方式1和方式3就是8位(无校验位)还是9位(有校验位)的区别,都可以调整波特率,波特率和T1溢出有关。方式2的波特率只有两种,fosc是晶振频率也就是时钟频率

方式0其实是同步的,RXD用来收发数据(收和发的时间隔开),然后TXD输出时钟,来指示下一位发出的时间,每来一个触发沿,RXD就输出下一位的东西。

发送的时候,如果选择了9位,那么第9位就是TB8,你自己设置好再让它传。而接收的时候如果有第九位会放在RB8里面。总是有的那八位就是进SBUF的

image.png

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,就不会发出中断,于是就不处理收到的数据。

更加完整的流程是这样的

image.png

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

image.png

对于8051来说,波特率=比特率(其他机器可能有差异,但这里我们不考虑),见到波特率你就等价看成bps就行

方式0的时候波特率是晶振频率/12,因为它就是按机器周期来的,下面提到的focs就是晶振频率

方式2的时候,如果SMOD=1就是focs/32,如果SMOD=0就是focs/64

方式1和3的波特率为\(\dfrac {2^{SMOD}} {32}T1溢出率\)

image.png

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

image.png

现在解释那个T1溢出率怎么回事。当T1用来做这个串口辅助的时候,设为方式2,也就是8位重装载的那个,而且是定时器模式,而且disable了T1的中断,就T1纯用来做这个串口输出的一个波特率发生器。定义T1溢出率为\(\dfrac {fosc}{12\cdot(2^8-初值)}\)

实际应用的时候会直接给表格,不用自己算这个东西,但是公式可能需要记忆一下。

image.png

举一个用中断的例子

;--------------------------------------------------------------------
; 文件名: uart_echo.asm
; 功能: 8051串口中断回显
; 晶振: 11.0592MHz -> 波特率9600
; 串口模式: 模式1 (8位UART)
;--------------------------------------------------------------------

                ORG     0000H
                LJMP    MAIN

                ; 串口中断向量地址
                ORG     0023H
                LJMP    UART_ISR

                ORG     0030H
MAIN:
                ; 1. 串口初始化
                MOV     SCON,   #50H      ; 模式1, 允许接收 (SM0=0, SM1=1, REN=1)
                MOV     TMOD,   #20H      ; 定时器1, 模式2 (8位自动重装)
                MOV     TH1,    #0FDH     ; 9600波特率 @ 11.0592MHz
                MOV     TL1,    #0FDH
                SETB    TR1               ; 启动定时器1

                ; 2. 中断使能
                SETB    ES                ; 允许串口中断
                ;不需要使能T1的中断
                SETB    EA                ; 总中断允许

                ; 3. 主循环: CPU可以在这里做任何其他工作
MAIN_LOOP:
                ; 在这里可以添加其他任务代码, 串口收发由中断处理
                SJMP    $

;--------------------------------------------------------------------
; 串口中断服务程序
; 说明: RI和TI都有可能触发中断, 但这里只处理接收中断(RI)
;       收到一个字节后立即回发
;--------------------------------------------------------------------
UART_ISR:
                PUSH    ACC               ; 保护现场
                PUSH    PSW

                JNB     RI,  CHECK_TI     ; 如果不是接收中断, 跳去检查TI
                ; 是接收中断
                CLR     RI                ; 清除接收中断标志
                MOV     A,   SBUF         ; 读取收到的字节到累加器
                MOV     SBUF, A           ; 立即写回发送寄存器(启动发送)

CHECK_TI:
                JNB     TI,  ISR_EXIT     ; 如果不是发送中断, 退出
                ; 是发送中断(由于我们在接收中断里触发了发送, 发送完后会进入这里)
                CLR     TI                ; 清除发送中断标志
                ; 本示例中无需额外操作, 直接退出

ISR_EXIT:
                POP     PSW               ; 恢复现场
                POP     ACC
                RETI                      ; 中断返回

;--------------------------------------------------------------------
                END
然后串口的寄存器我也做了总结

image.png

要根据要求的波特率配T1,所以也要用到前面的图

串口比较麻烦的就是还有个SMOD要记得,它不能位寻址,只能整个赋值给PCON,然后SCON就和TMOD那样具有相同的地位,虽然SCON可以位寻址,但是还是推荐用MOV一次配好,一个一个配太麻烦了

如果使用轮询模式,无需配置中断相关的ENABLE和PS,只需要自己查询TI和RI,比如用JB和JNB来判断是否要执行下一条语句,其实我感觉一般用轮询是比较简单的

方式1应用

image.png

这里关于奇偶校验位缺了很多前置信息,直接看看不懂

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当作校验位,应当是偶校验位)

这个程序能自己写出来就算是中断,定时计数器,串口过关了

image.png

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

image.png

多机通信

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

image.png

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

image.png

意思是在模式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)

image.png

image.png

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

注意这里延时程序的应用,如果一直运行可能太快了

image.png

加了一个锁存器就是这样的,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内置的

image.png

image.png

当-LE1=0,则第一个寄存器锁存,当-LE1=1,则第一个寄存器的输出和输入相同,对第二个类似

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

image.png

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

image.png

这里是-LE2永远为1,而-LE1由A15和WR控制

第三种接法,通过-WR1和-WR2控制两个的锁存状态,用-CS和-XFER来做片选

image.png

A15=0选中第一个寄存器,A14=0选中第二个寄存器

双缓冲型通常用于多个同步输出的时候

image.png

先用FDH控制把数据写进第一个DAC0832的第一个寄存器存着,再用FEH把数据写进第二个DAC0832的第一个寄存器存着,再用FFH一起控制第二个寄存器,就可以实现同时输出了

下面是12位的,考试不考*

如果想要更多位的DAC,必须要用这种同步的接法,不然你先传低八位再传高四位,那传高四位之前的状态就不是你想要的状态,要避免这种状态出现,所以需要用这种方式来锁存

可以这样搞

image.png

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

image.png

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

image.png

接法是这样,其中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信号就是模拟信号变成数字信号,模拟信号比如现实中的声音和光,被记录在手机里就是变成了数字信号。

介绍ADC的两种类型,逐次比较型和双积分型

image.png

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

image.png

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

image.png

然后运算出来ux就行了,然后进行输出

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

image.png

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

image.png

当-EOC为低电平,说明已经采集完毕,你可以从P口取了。EOC是end of convertion的意思。然后START是要手动复位的,而EOC是它自动复位的

注意START和EOC是针对一次采样的信号,所以它们的复位也要在循环里面。JB P2.1, WAIT就是等待-EOC信号变成低电平,那就可以去取了,之后就把P1的弄进去就行

注意-START在CLR之后要DLY然后再SETB

image.png

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

image.png

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

image.png

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

介绍集成ADC

ADC0809

image.png

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

image.png

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

image.png

image.png

这里只演示了一个通道的处理方式,没有写循环,否则应当给DPTR移动到下一个通道而且也多设一个存数据的指针

键盘和显示器接口

考的很少,了解独立式键盘和矩阵式键盘的差异和矩阵式键盘,以及动态扫描显示的原理即可,专用芯片都不考

键盘

键盘分为编码式和非编码式,编码式键盘带译码器,硬件完成编码。非编码式内部没有译码器,只能返回按键在第几行第几列,由软件来明确这个位置对应的编码。微机原理里用非编码式的多,这里只介绍非编码式

非编码式的又分独立式和矩阵式,独立式就是一个口线接一个按键,读取口线状态知道是哪个,矩阵式按键会对按键做二维行列的排布。

要解决按键机械抖动的问题,理想情况是没按的时候高电平,按下低电平,放开又回到高电平,但是实际上在按下和松开的时候都有一段时间在抖动,有时高电平有时低电平。

image.png

一种方法是用RS触发器

image.png

因为抖动只是小范围的抖动,只要一次R或S有效之后,就已经把状态保存了,就算抖动也不会改变

第二种方法是延时再去判断,延时10-20ms再看,这个是通过软件实现,RS触发器是硬件的方式。抖动在10ms以内结束,而人的动作速度最快在百毫秒级别,所以这是可行的

对于按键长按,有两种解读方式,一种是认为按下只视为一次操作,只会输出一次,另一种是发现你一直按着的时候就以一个固定频率输出这个按键对应的值。

查询法

独立式

image.png

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

image.png

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

矩阵式

image.png

其中行用了三个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有两种接法

image.png

想要控制亮暗,可以通过调节占空比来实现

有静态和动态的两种显示方式,静态就是显示字符的时候亮就一直亮,没有周期变化,动态就是字符显示的时候有亮的时候也有不亮的时候

静态的

image.png

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

image.png

动态的

image.png

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

系统设计案例

image.png

image.png

image.png

image.png

image.png

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

image.png

image.png

image.png

image.png

image.png

image.png

本页总阅读量