外部拓展其实是个相对来说很好玩的章节,可以真正开始用单片机写程序了,比较重要的是外部存储器拓展,81C55拓展,矩阵键盘,动态显示,DAC和ADC。
0. IO接口电路概念与存储器拓展
1. 为什么需要IO电路?:1. 协调计算机与外设的速度的差异 2. 输入/输出过程中的状态信号 3. 解决计算机信号与外设信号之间不一致
2. IO传送方式三种:1. 无条件传送(灯,DAC),2. 查询,3. 中断(ADC)。
3. DMA存储方式(直接传输数据不通过CPU,这种方式实际上已经很古老了,都快要被淘汰了(当然还存在STM32,51这些低级产品中)。)
工作流程:
①CPU通过指令,把要传送的数据块的长度、传送数据块在内存中的首地址等信息写入DMA控制器。
②外设通过DMA控制器向CPU发出DMA请求。
③CPU接受DMA请求后,暂停正在执行的程序,并且放弃对总线的控制权,由DMA控制器来接管,外设的数据线和存储器的数据线经过DMA控制器的通道直接连通。
④DMA控制器通过地址总线向存储器发出传送数据的地址。
⑤如果是外设向存储器传送数据,DMA控制器向外设发出读信号,读出数据,DMA控制器向存储器发出写信号,则写入数据,由于两者的数据线是直接连接的,数据读、写的操作就可以连续进行,很快地完成一次数据传送。
⑥数据块长度计数器减1,然后重复以上进程,直到数据块传送完毕。
⑦DMA操作结束,CPU再次恢复对总线的控制,继续执行原来的程序。
以上的传送过程,一次数据传输一般只要几个时钟周期就可以完成。
4. 在无条件传送方式下,CPU和外设端口之间也要有接口电路。
1. 在输入端口上会有一个输入缓冲器:
在不做输入操作时,缓冲器处于高阻状态,CPU实际上和输入外设没有连接;
需要做输入操作时,地址译码器的输出使缓冲器正常工作,输入设备的信息就可以通过缓冲器读入到CPU。
2. 在输出的端口上一般会有一个输出锁存器:CPU将要输出的信息存入输出锁存器中,外设从锁存器读取信息。
接口电路也至少需要两个端口:状态端口和数据端口,用以分别传送状态信息和数据信息。(这就是ADC芯片所设计的那样的。)
5. 单片机从逻辑上有3个存储器寻址空间
片内RAM空间:00H~FFH
片外RAM空间:0000H~FFFFH
片内外统一编址的ROM空间:0000H~FFFFH (注意是片外,如果是片内+片外为0000~0FFFH,然后片外再从1000H开始到FFFFH)
6. 扩展ROM时,用控制线PSEN,指令用MOVC
扩展RAM时,用控制线RD和WR,指令用MOVX
单片机是通过地址(AB)、数据(DB)和控制总线(CB)与外部交换信息的。
1. 存储器的拓展(ROM & RAM)
1. 拓展程序存储器(以拓展2732为例)(一定要会)
现在我们来解释一下这些引脚的意思:
ALE引脚:和我们之前的说的是一样的,就是实现对P0口的分时复用,要注意这个ALE很坑的地方是他是在下降沿P0口的地址输出信号才有效,所以锁存芯片一定要选对(比如这里选的就是373,如果选那些上升沿触发的377之类的,请加个反相器再加上去)。
PSEN引脚:引脚功能如其名:外部程序存储器选通信号PSEN,所以接2732的使能片很符合常理。(低电平有效)
2732的CE引脚:片选信号端,用P2.5~2.7进行选通。
关于时钟,ALE,PSEN,P2,P0的时序图:
注意ALE和PSEN是同步开始的,P2和P0的信号相对于PSEN和ALE都是有延迟的
译码器法:(其实就是加了个译码器而已,简单)
2. 拓展外部RAM(以拓展6116为例)(一定要会)
SRAM(51的内部内存都是SRAM)通常用于小于64KB的小系统,DRAM(我们的电脑内存是DRAM)用于大于64KB的大系统。
拓展RAM和ROM的做法其实是差不多的,但是唯一的不一样是ROM是拓展的PSEN,所以PSEN要接ROM的OE,但是对于RAM来说,它读写双向的,所以我们只要拓展把80C51的RD端接6116的OE端,WR端接6116的WE端就可以了,同时注意,6116也是有片选端(CE)的,我们也可以像ROM那样对RAM进行片选。
3. 拓展FLASH(RAM + ROM)
就是把ROM和RAM的拓展方式结合起来,既要接PSEN又要接WR和RD
注意这里的PSEN,WR,RD,WE,CS和OE都是低电平有效的,一旦哪个引脚低电平了就可以把FLASH对应成相应存储器,比如当PSEN为低电平时,这个Flash就是个程序存储器,WR为低表示以外部RAM写,ED为低则表示以外部RAM读(可能这个电路有点复杂,其实只用仔细看一下就知道只要有一个是低电平CS就会是低,然后你就可以把它看成是什么都行了)。
注意这里的拓展还用P1拓展了地址,输出的时候注意:
;(写)
MOV P1,#05H
MOV DPTR, #0AAAAH
MOV A, #3FH
MOVX @DPTR, A
;(读)
MOV P1,#02H
MOV DPTR, #0AAAAH
MOVX A, @DPTR
;地址由P0,P1.0~2,P2共同决定
当然了我们也可以不占用P1来拓展这个芯片,由于我们只用保证必须要有信号就可以了,所以我们可以用锁存器来继续拓展这个芯片,这样就可以只用P0~P2了(程序设计要麻烦一点而已)。
4. 奇葩拓展芯片2718(有BUSY和READY位告知CPU已经完成转换)
仔细看下RD和PSEN,他们是通过一个与门来连接同样是低电平有效的OE,这个芯片的特别之处在于他有BUSY位(低电平有效)和RDY(Ready)位,看英文就知道是怎么回事。
5. IO口拓展
这个有个很有趣的例子就是拓展开关和灯泡,拓展IO口可以很巧妙地里用WR和RD不同时为0的特性:
还记得之前我们是怎么说的吗?输出部分要有锁存器,输入部分要有缓冲电路。74LS244为3态8位缓冲器,一般用作总线驱动器。74LS273是8位D触发器,常用作锁存器。这个图完美的切合了我们之前所说的,由于WR和RD不同时为低,所以我们可以仅用MOVX指令就可以完成IO拓展了(P2.0当总控制口)
上例中按下任意键,对应的LED发光。
CONT:
MOV DPTR, #0FEFFH ;数据指针指向口地址
MOVX A, @DPTR ;检测按键,向74LS244读入数据
MOVX @DPTR, A ;向74LS273输出数据,驱动LED
SJMP CONT ;循环
总结一下,如果出了设计题,一定要先思考到WR和RD的问题,要记住WR和RD都是低电平有效的,一般和与门进行结合来控制CS端。
1. 81C55拓展
1. 81C55参数如下:(要熟记81C55的引脚的作用)
AD7~AD0:三态地址数据总线,用于传送数据、命令和状态字,分时复用线,P0口可以和AD0~AD7直接相连接。
CE:片选信号线。(80C51随便找个P口控制)
RD:存储器读信号线。(一般接80C51的RD线)
WR:存储器写信号线。(一般接80C51的WR线)
ALE:地址及片选信号锁存信号线,高电平有效,其后沿将地址及片选信号锁存到器件中。 (一般接80C51的ALE线)
IO/M:I/O接口与存储器选择信号线,高电平表示选择I/O接口,低电平选择存储器RAM。(80C51随便找个P口控制)
PA7~PA0:A口输入/输出线。
PB7~PB0:B口输入/输出线。
PC3~PC0:C口输入/输出或控制信号线(A口和B口作为选通口时)。
TIMER IN:定时器/计数器输入端
TIMER OUT :定时器/计数器输出端
RESET:复位信号线
81C55各端口地址分配(81C55一共7个端口)(注:那个C/S寄存器就是命令字/状态字寄存器(共用一个地址,但是是不同的两个寄存器),低电平有效)
引脚作用(注意0是操作RAM,1是操作IO)
命令字(只能写)
状态字(只能读)
TIMER:定时/计数器中断请求标志,定时器/计数器记满时这个位为1,当CPU读取状态后,这个标志位为0
INTE:端口允许中断位,高电平表示允许对应口中断,低电平表示禁止对应端口中断
BF:对应端口的缓冲器状态标志位,高电平表示的是缓冲器填满,低电平表示的是可以接受外设或者单片机的数据。
2. 当81C55涉及C寄存器做联络线的问题
其实这个问题是很简单的,C寄存器的6个口的功能如下
我们知道81C55可以很方便地与慢速设备进行连接,当外设需要往PA或者PB输入数据时,外设首先先给BSTX端口一个低电平信号,向81C55表明需要读信号,然后81C55的PX就从外设中开始读数据,到读满端口寄存器时,给ABF/BBF置位(表明缓冲区满),并且向CPU发出中断。CPU(比如80C51)调用中断处理,只需要一条MOVX A,@DPTR(此时DPTR指向对应P口),就可以把数据读进来。
当需要写数据时,只要80C51一条MOVX @DPTR, A,就可以给对应P口写数据,直到对应P口的寄存器满,对应的BF位置位,外设就可以从P口读数据了,读数据时,BSTB变成低电平。
3. 关于81C55的定时器
81C55的定时器是14位的减1定时器
定时器寄存器的地址上面有写,需要注意的是,定时器的高8位的最高两位是设定81C55定时器的工作方式的:
单负方波:计数期间输出为低电平,记满回“0”后输出高电平。
连续方波:计数长度的前半部分输出高电平,后半部分输出低电平,如果计数值为奇数个,则高电平为(n+1/2)个,低电平为(n-1/2)个。连续方波输出方式能自 动恢复初值。
单负脉冲:计数器记满回“0”后输出一个单负脉冲。
连续脉冲:计数值回“0”后输出单负脉冲,然后自动重装初值,回“0”后又输出单负脉冲,如此循环。
给命令字的TM0和TM1位设定对应值即可开启定时器~
例题:81C55的命令字寄存器的地址是7F00H,要求设定81C55的A口为基本输入方式,B口定义为基本输出方式,C口输入,打开定时器,读取81C55,要求将立即数0AAH写入81C55 RAM的7E25H单元:(P2.0是接CE,P2.7接IO/M)
MOV DPTR,@7F00H
MOV A,#0C2H ;11000010,(打开了计时器A口为输入(0),B口为输出0,PC1:PC0 = 00(A/B基本输入输出,C口输入))
MOVX @DPTR,A
MOV A,#0AAH
MOV DPTR,#7E25H ;写入的是内存,地址是7E25H,刚好符合写入RAM的要求(81C55的RAM只有256B)
MOVX @DPTR,A
2. 矩阵键盘拓展
矩阵键盘只要记住矩阵键盘的样子:
你就能想到矩阵键盘的扫描方法了(必须按行和列来扫,因为本质上是通过给行/列全部低电平来让判断按钮有没有按下,当某行某列有按钮按下,对应电阻就会产生电压降,我们就可以找到对应的按钮了)。
来一段我以前写过的C的代码:
#include
sbit encoder_selet=P2^7;
sbit numeric_display=P2^6;
unsigned char leddata[]={
0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07,0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E,
0x79, 0x71, 0x76, 0x38, 0x37, 0x3E, 0x73, 0x5C,0x40, 0x00, 0x00
};
unsigned char lower_key_set[4]={0x0e,0x0d,0x0b,0x07};
unsigned char lower_key_set_colum[4]={0xe0,0xd0,0xb0,0x70};
void delay(unsigned int);
void scan_keyboard(int);
void scan_keyboard_col(int);
void display_key(int);
static int pos= 0;
int main(void)
{
int pos = 0;
bstv51_init();
while(1)
{
scan_keyboard_col(0);
scan_keyboard_col(1);
scan_keyboard_col(2);
scan_keyboard_col(3);
}
return 0;
}
void scan_keyboard(int line)
{
unsigned char in, lower_key,in_tmp;
int selet_key;
lower_key = lower_key_set[line];
P3 = lower_key|0xf0;
in = P3;
in_tmp= in & 0x0f;
in &= 0xf0;
in |= lower_key;
if(in!= (lower_key|0xf0))
{
delay(5);
P3 = lower_key|0xf0;
in = P3;
in &= 0xf0;
in |= lower_key;
if(in != (lower_key|0xf0)) //软件防抖
{
in_tmp = in & 0xf0;
switch(in_tmp) //找到相应的坐标
{
case 0xe0:
selet_key= 0 + line*4;
break;
case 0xd0:
selet_key= 1 + line*4;
break;
case 0xb0:
selet_key= 2 + line*4;
break;
case 0x70:
selet_key= 3 + line*4;
break;
}
while(in != (lower_key|0xf0))
{
in = P3;
in &=(lower_key|0xf0);
}
display_key(selet_key);
}
}
}
void scan_keyboard_col(int colum)
{
unsigned char in, lower_key,in_tmp;
int selet_key;
lower_key = lower_key_set_colum[colum];
P3 = lower_key|0x0f;
in = P3;
in_tmp= in & 0x0f;
in &= 0x0f;
in |= lower_key;
if(in!= (lower_key|0x0f))
{
delay(5);
P3 = lower_key|0x0f;
in = P3;
in &= 0x0f;
in |= lower_key;
if(in != (lower_key|0x0f)) //软件防抖
{
in_tmp = in & 0x0f;
switch(in_tmp) //找到相应的坐标
{
case 0x0e:
selet_key= 0 + colum*4;
break;
case 0x0d:
selet_key= 1 + colum*4;
break;
case 0x0b: