外部拓展其实是个相对来说很好玩的章节,可以真正开始用单片机写程序了,比较重要的是外部存储器拓展,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 <reg51.h> 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: selet_key= 2 + colum*4; break; case 0x07: selet_key= 3 + colum*4; break; } while(in != (lower_key|0x0f)) { in = P3; in &=(lower_key|0x0f); } display_key(selet_key); } } } void display_key(int selet) { P0=0x00; //全部点亮 encoder_selet=1; encoder_selet=0; P0=leddata[selet]; numeric_display=1; numeric_display=0; } void delay(unsigned int xms) { unsigned int i,j; for(i=xms;i>0;i--) //i=xms即延时约xms毫秒 for(j=112;j>0;j--); }
这个真的很简单。。。某些垃圾视频教学把其列为重点来教学,不能理解。
3. DAC转换
D/A转换的目的是把输入的数字信号转换成与此数字量大小成正比的模拟量
DA转换:
1. DA转换性能指标:
(1)D/A转换时间:D/A转换时间是指从一个数字量加载到DAC的数据输入端到DAC输出电压达到其最终电压的±1/2LSB范围内的时间。一般在几十纳秒到几百微秒的范围。
(2)分辨率:输出电压值之间的最小差值就是DAC的分辨率。
(3)D/A转换精度:D/A转换器实际输出电压与理论输出电压的偏差。通常以满输出电压VFS的百分数给出。
2. 重点例子:8位D/A转换器DAC0832(记住0832的一些引脚内容就可以了)
CS:片选信号输入端,低电平有效。
ILE:输入锁存使能信号,高电平有效。
WR1:输入锁存器写选通信号,低电平有效;为低,允许8位数据总线上的数据输入到输入锁存器中,为高,锁存输入锁存器中的数据。
WR2:DAC数据寄存器写控制信号,低电平有效。为低,且为低电平时,输入锁存器中的数据传输到DAC数据寄存器中,并自动开始进行D/A转换。
XFER:数据传输控制信号,低电平有效。与一起控制输入数据锁存器和DAC数据寄存器之间的数据传输。
DI7~DI0:8位数据输入总线。
其中由于这个芯片有一个缓冲器和一个寄存器,所以可以形成双缓冲模式,非常有趣
ORG 0000H AJMP MAIN_START ORG 0100H MAIN_START: MOV R0,#30H ;设置x数据指针 MOV R1,#50H ;设置y数据指针 MOV R2,#0 ;清计数器为零 ;输出x数据到DAC0832 U2的输入寄存器 GOON: MOV DPTR,#0DFFFH ;x数据DAC地址为DFFFH MOV A, @R0 MOVX @DPTR,A ;写数据到DAC0832 U2 INC R0 ;x数据指针指向下一个数据 ;输出y数据到DAC0832 U2的输入寄存器 MOV DPTR,#0BFFFH ;y数据DAC地址为BFFFH MOV A,@R1 MOVX @DPTR,A ;写数据到DAC0832 U3 INC R1 ;y数据指针指向下一个数据 ;把所有DAC0832的输入数据寄存器的数据写入到DAC转换寄存器,;1µs后同时输出数据到绘图仪 MOV DPTR,#7FFFH ;DAC转换寄存器地址为7FFFH MOVX @DPTR,A ;使能和启动D/A转换 INC R2 ;统计输出数据个数 CJNE R2, #20,GOON ;输出20个数据后,绘图结束 SJMP $ ;停机 END
注意由于电路的WR接了WR1和WR2,所以只要是写操作并且开了CS都会让数据送到寄存器上,当P2.7 = 0时,XFER和WR2同时为低,就把数据一起输出了。这样就可以在不用外接锁存器的情况下实现双缓冲(Windows编程里面的双缓冲也是基于这个原理的)。
AD转换:
1. A/D转换的技术指标:
量程:指A/D转换芯片所能转换的模拟输入电压的范围
分辨率:对微小输入量变化敏感程度的度量。
转换时间与转换速率:指从模拟量输入到转换结束输出数字量所需要的时间 ,转换速率则是转换时间的倒数。
2. 重点例子8位A/D转换器ADC0809:
IN0~IN7:8路模拟信号输入引脚。
D0~D7:8位A/D转换输出数据总线。输出数据与输入电压的关系式为
其中,[ ]表示取整运算。
A、B、C:模拟通道地址选择信号,A为最低位,C为最高位。
SC:转换开始引脚,正脉冲有效。
EOC:转换结束标志信号,高电平有效,表示转换结束。在与处理器连接时,此引脚可以用来向处理器发出中断,或者是供处理器软件查询。
转换步骤:(直接看代码就好了)
第一步:地址信号到ABC;ALE在ALE的上升沿锁存地址信息,选定通道;
第二步:模拟信号输入到ADC的模拟输入脚,启动SC(一般由80C51的WR脚发出,也就是使用MOVX指令(C51使用xdata的指针),看后面的电压表例子),一定时间内开始转换,转换结束后EOC变高(这个脚一般接80C51的外部中断口)。(注意一般SC和ALE与80C51的WR相连,OE和80C51的RD相连)
第三步:此时可以直接从D0~D7(注意地址要对)拿出数据。(拿出数据时因为有OE的作用所以数据不会变)
3. 单片机与ADC0809的两种连接方法:
;通道1的选择及启动转换 MOV DPTR, #8001H MOVX @DPTR, A ;此处A的值无关紧要
MOV DPTR, #8000H MOV A, #01H MOVX @DPTR,A;此处A的低3位为ADC0809的地址信号
小例题:锯齿波输出:
ORG 0000H LJMP START ORG 0100H START: MOV DPTR, #07FFFH MOV A,#0 MOV R1,#0 OUTPUT_UP: MOVX @DPTR,A ADD A,#4 INC R1 NOP NOP NOP NOP CJNE R1,#51,OUTPUT_UP MOV A,#0 MOVX @DPTR,A LJMP START END
结果:
4. 例题
1. 动态显示(C51)
#include <reg51.h> #define SUM 4 typedef struct _PanelNumber { unsigned char digitalNum; unsigned char port; }PanelNumber; void delay(void); int main(void) { PanelNumber numberSet[SUM]={{0xF9,0x70}, //num:1 port:P2^7 {0xA4,0xb0}, //num:2 port:P2^6 {0xB0,0xd0}, //num:3 port:P2^5 {0x99,0xe0}}; //num:4 port:P2^4 int i; while(1) { for(i = 0;i!=SUM;i++) { P2 = 0xff; P2 = numberSet[i].port; P0 = numberSet[i].digitalNum; delay(); } } return 0; } void delay(void) { unsigned char i,j; for (i=0;i<30;i++) { for (j=0;j<170;j++); } }
2. 电压表(C51)
voltmeter.c
1 #include <reg51.h> 2 #include "voltmeter.h" 3 4 static NumberPanel numberPanel[3]; 5 6 int main(void) 7 { 8 IT0 = 1;//failling edge trigger 9 EX0 = 1;//enable interrupt 0 10 EA = 1;//enable global interrupt 11 12 numberPanel[0].outputPort = 0xfbff; 13 numberPanel[1].outputPort = 0xfdff; 14 numberPanel[2].outputPort = 0xfeff; 15 16 START_ADC(adcAddress);//start adc transformation 17 18 while(1); 19 return 0; 20 } 21 22 void OnADCTransform_Completed() 23 interrupt INT0 using REGISTER_0 24 { 25 unsigned char result = 0; 26 float rResult; 27 28 READ_DATA(result,adcAddress); 29 30 rResult = (float)result * 0.0196078; 31 rResult *= 100; 32 33 translateResult((int)(rResult)); 34 35 START_ADC(adcAddress);//restart adc transformation 36 } 37 38 void translateResult(int result) 39 { 40 int dividend = 100,i = 2; 41 unsigned char number; 42 43 for(;dividend != 0; dividend /= 10, i--) 44 { 45 number = digitalNumberSet[result/ dividend]; 46 result %= dividend; 47 48 //open WD and output the data, 49 //the data will lock in the register while WD is low level 50 OUTPUT_DATA(i,number); 51 } 52 }
voltmeter.h
1 #define INT0 0 2 #define REGISTER_0 0 3 #define START_ADC(a) ((*a) = 0) 4 #define OUTPUT_DATA(i,data) ((*(numberPanel[i].outputPort)) = data) 5 #define READ_DATA(data,add) (data = (*add)) 6 7 //choose channel 0, while P2.7 is low level 8 unsigned char xdata *const adcAddress = 0x0ff8; 9 10 typedef struct _NumberPanel 11 { 12 unsigned char xdata *outputPort; 13 }NumberPanel; 14 15 unsigned char const digitalNumberSet[] 16 = {0xC0, 0xF9, 0xA4, 0xB0, 17 0x99, 0x92, 0x82, 0xF8, 18 0x00, 0x90, 0x88, 0x83, 19 0xC6, 0xA1, 0x86, 0x8E}; 20 21 void translateResult(unsigned char result);