上手
- 硬件环境普中A2学习板,STC89C52单片机;
- 单片机开发软件:keil5 uvision5(选C51),软件需要授权;
- 单片机下载软件:STC-ISP,软件不需要安装,直接点击运行。但是运行前PC需要预装CH340串口驱动;
CH340目的是实现USB转串口(UART)
- STC89C52介绍
- 为什么叫51?指只要兼容8051内核的单片机都可以叫做51单片机。
- 什么是8位单片机?指的是CPU中的
ALU,算术和逻辑运算单元
的位数。8位的代表就是51单片机,表示数据总线的宽度是8位
,它一次可以计算两个8位数据二进制相加,如果数据是16位/32位的,就要分成多次来计算。但对于32位单片机而言,计算32位的数据相加,只需要一次计算,速度上就有提升。 — >本质是指它的ALU一次性可以最多处理多少数据位。若理解为参与运算的寄存器的数据长度也行。
这里提到的位也可以称作单片机的字长
。 - RAM512B怎么算的?
片内RAM
:标准51(片内128B)的片内RAM地址从00H~7FH共128B(16#7FH转换为10#127 + 00H占1位= 128B),而高128B(16#80H~16#FFH )给到SFR,特殊功能寄存器
,低128B可以用直接或者间接寻址方式访问,一般算作片内;SFR只能通过直接寻址方式访问,算作片外。对于C52(片内256B)单片机而言,低128B(00H~7FH )位依旧可以直接和间接寻址,高128B(16#80H~16#FFH )只能用作间接寻址。但是SFR的地址依旧为高128B(16#80H~16#FFH )和用户高128B地址重叠,但只能用直接寻址
。区别方法是通过指令的寻址方法的不同来区分
。51单片机比较另类的地方:寻址内存的空间,不靠总线,靠指令的使用来区别。片外RAM
:片外RAM是可拓展的,最大可以拓展到16#0000~16#FFFF共64KB(2^16-1=65535Byte,约等于64KB)。另外要注意的是片内RAM和片外RAM的地址不是连在一起的,如片内0x00和片外0x0000是不同的。
片内和片外RAM的定义最早来自于早期51单片机,分别指芯片内部和芯片外部,但是现在几乎所有的51单片机都在芯片内部集成了片外RAM,而真正的芯片外拓展已经很少用到。所以虽然她叫片外RAM,但是它还在芯片内部,具体区分如下:- data:片内RAM,从0x00~0x7F;
- idata:片内RAM,从0x00~0xFF ;(虽然data包含在idata内,但是所带来的访问速度是不同的。在keil的默认设置下,data是可以省略的,即什么都不加时变量自动定义到data区域。data区域使用直接寻址,访问速度时最快的;使用idata之后用的是通用寄存器间接寻址,速度会比data慢一些。而且大部分时候我们不太希望直接访问到16#80H~16#FFH,因为它们很多时候用来做中断和函数调用堆栈,所以大部分情况下,在使用的时候直接用data即可。)
- pdata:片外RAM,从0x00~0xFF ;(用通用寄存器间接寻址。)
- xdata:片外RAM,从0x0000~0xFFFF ;(寻址范围从0-64K,但是需要使用2个字节寄存器DPTRH和DPTRL来间接寻址才行,它的访问速度是最慢的。)
- 当前使用的STC8952共有512B的RAM,其中片内256B和片外256B。
- 为什么说外部拓展最高可以拓展到64KBYTE?因为
DPTR
寄存器是16位(2^16-1=65535Byte,约等于64KB)的。
- 51单片机的
地址总线是16位
的:- P0(P0.0~P0.7)口:双向IO口,既可作为
低8位的地址总线
,又可作为8位双向数据总线,分时复用。当P0口作为通用IO口时,需要外接上拉电阻以增加驱动能力。 - P2(P2.0~P2.7)口:准双向IO口,可作为
高8位的地址总线
。P2是动态的IO口,虽然输出能被所存,但是不能稳定的输出。
- P0(P0.0~P0.7)口:双向IO口,既可作为
- 单片机中RAM的发展:
- 最开始,使用8位的地址总线,那么所能寻址的最大宽度就是256B(从0到2^8-1=255字节)。
- 后来,地址总线拓展到16位,最大寻址宽度变成了2^16-1=65535Byte,约等于64KB。
- 32位CPU的出现,不仅带来了32位的数据总线,也带来了32位的地址总线,此时的最大寻址宽度变成了2^32,4亿多字节,合下来4GB,在嵌入式领域基本用不完了。
- 随着PC和手机的发展,64位处理器出现,相应的地址总线也增加了数倍,以满足更高要求的使用场景。
- ROM:8K,flash。
- 51的外部晶振选用11.0592MHZ或12MHZ:
- 1个机器时间的周期=12次振荡/震荡频率=12/(12MHZ)=1us;如果用11.0592那么除不尽,所以做定时器延时的时候用12MHZ更加准确。
- 11.0952MHZ=11059200=115200*96,可以直接分频到115200以及所有需要的波特率(9600/115200/19200/38400/57600bps)。所以11.0592MHZ可以满足常见的串行通信标准波特率。
- 1个机器周期(=12个振荡周期)指令往下读一次。所以如果没有晶振电路,程序就执行不了。
- 89C52的最小系统:
VCC系统电源用C6/C5电容的目的是滤波,把交流干扰导入GND;
RST电路上电瞬间C1电容充电,此时会把RST引脚接通,实现单片机上电复位;
晶振电路是必要的,没有晶振程序不能运行;
-
STC89C52 35I-DIP40命名规则:
- STC89:STC公司 12T/6T 8051系列
- C:表示工作电压5.5~3.8V
- 52: 表示8K字节ROM和512字节RAM
- 35: 工作频率可到35MHZ
- I: 工业级,-40℃到85℃
- DIP: 封装类型PDIP
- 40: 40管脚数
-
关于IO口:
- 带锁存和不带锁存的IO口的区别:不管IO口带不带锁存,
都可以实现数据的保持
(意味着就算没有在while中反复循环置位。而只用触发一次,也能使信号被保持),但是区别在于带锁存器的IO口抗干扰能力强,输出稳定,不会因为外部环境的干扰导致而使得IO口的状态发生变化;而不带锁存器的IO口可能会由于外部干扰导致信号状态变化。对于不带锁存的IO口,可以选择利用while循环反复刷新以对抗外部干扰。另外,不带锁存器的IO口意味着结构简单,响应速度更块,适合一些高速数据传输
场景。 - 除了P0口没有内部集成上拉电阻以外(需要外接上拉电阻),其他三个口内部都有上拉电阻。
- 带锁存和不带锁存的IO口的区别:不管IO口带不带锁存,
准双向口
-
什么叫
准双向IO口
:51单片机的P0/P1/P2/P3都是准双向IO口。它即可作为输入,也可以作为输出。但其输出高电平时依赖上拉电阻,驱动能力很弱(弱上拉)。准双向口时51单片机的一大特点。 -
准双向口特点:
- 默认状态为高电平(弱上拉):
- 如果没有外部施加信号,IO口会保持高电平。
- 在复位后,准双向口默认配置为输入模式,并通过一个弱上拉电阻连接到高电平。
- 输出时,主动驱动:
- 当程序向准双向口写入0时,IO口会主动输出低电平(相当于导线接通,使得电流可以从外部流入IO口,从而驱动外部器件)。
- 当程序向准双向口写入1时,IO口会释放驱动,回到高阻状态(依靠若上拉电阻保持高电平)。
- 输入时,依赖外部信号:
- 若需要配置输入模式,需要写入1(
keil的头文件已经默认配置了,如果是汇编,那就需要人工置1
),保证其已进入高阻态(或叫做禁止状态)
。 - 高阻态下,外部信号可以驱动该引脚,单片机
可以读取到外部信号的电平
。
- 若需要配置输入模式,需要写入1(
- 弱上拉电阻的作用:
- 弱上拉电阻的存在使得准双向口在未被驱动时默认为高电平,避免引脚悬空。
- 默认状态为高电平(弱上拉):
-
准双向口和真正的双向IO口的区别:
- 准双向口:
- 默认有上拉电阻。
- 输出高电平是通过
释放驱动(高组态)
实现的,而不是主动驱动高电平。 - 切换为输入模式需要通过
写1
来实现。
- 真正的双向口:
- 没有弱上拉电阻。
- 输出高电平是主动驱动的。
- 输入和输出模式的调整通常是通过
专门的方向寄存器
来配置。
- 准双向口:
-
高电平和高阻态的区别,如配置P0.0=1;如何判断P0.0当前是高电平还是高阻态:
- 高电平:
- P0.0引脚的电压为高,接近VCC
- 高电平可能是因为内部或者外部电路主动拉高的。
- 高阻态:
- P0.0引脚
内部电路完全断开
,单片机不主动输出任何电平。引脚状态完全由外部电路决定,可能是高电平(弱上拉)或者低电平(外部下拉)。
- P0.0引脚
- 判断方法:
- 配置P0.0=1后,执行if(P0.0==1):若为真,则说明此时为高电平;若为假,则说明P0.0引脚被外部拉低,此时P0.0为高阻态,外部电路将引脚拉低。
- 高电平:
理解地址和数据的关系
- 通俗的解释地址和数据的关系(地址的宽度表达的是数据):把地址单元比作一个一个的机器。那么cpu 总共能标识的机器数量就是地址寄存器最大所能表达的数量(地址寄存器类型和芯片地址总线相关,不是随便定义的),再多就溢出了。每个机器都有唯一的编号(以数量依次排开),所有编号的格式(数据类型)是统一的。地址的宽度(若地址单元为1byte,那么int类型就是2个地址单元的宽度)用来表达数据(int最多就表达2^16 这么多内容)。数据就好比机器所能生产出来的产品类型(假如一个地址单元是1byte,那么这个地址单元就能生产出2^8 =256种产品)。另外来看,内存或者硬盘存数据,其实存入的数据的大小就是数据占用的实际地址空间。我们读取数据的时候,选择相应的地址片段的长度,就可以得到这个片段长度表达出来的内容,内容即数据,这样就得到想要的数据。
使用keil5
- 新建Source Group: 找不到厂家STC的名字,可以找
Atmel--AT89C52
。 - 新建程序文件: 右键Source Group文件夹,若选择C语言,会自动建立c文件。
- 选择生成HEX文件
勾选
Create HEX File
才能生成供STC-ISP下载的文件。
- 编译文件。
- 利用STC-ISP下载程序。
- 点击下载后,冷启动单片机,下载才会成功:
驱动LED
- 开发板原理图:
如原理图,P2控制LED
VCC供电,电流从VCC流向P2口,所以 =0,灯亮/ =1 灯灭
- 用到P2口,程序编辑器界面按
#
,鼠标右键点击加入头文件; - 不识别2进制(P2=11111110),需要转换成16进制显示(P2=0xFE)
#include <REGX52.H> //头文件 void main() { while(1) { P2=0xFE; //11111110,亮点第一个 P2=0xFF; //全灭 P2=0x55; //01010101 } }
#include <REGX52.H> //头文件 #include<INTRINS.H> //实现延时 void Delay500ms() //@12.00MHZ { unsigned char i,j,k; //unsigned char: 0~255 _nop_(); //表示空语句,什么都不做,需要引用头文件 #include<INTRINS.H> i = 4; j = 205; k = 187; do { do { while(--k); }while(--j); }while(--i); } void main() { while(1) { P2=0xFE; //11111110 Delay500ms(); //调用延时方法 P2=0xFF; Delay500ms(); P2=0x55; //01010101 Delay500ms(); //流水灯 P2=0xFE; Delay500ms(); P2=0xFD; Delay500ms(); P2=0xFB; Delay500ms(); P2=0xF7; Delay500ms(); P2=0xEF; Delay500ms(); P2=0xDF; Delay500ms(); P2=0xBF; Delay500ms(); P2=0x7F; Delay500ms(); } }
-
利用STC自动生成延时函数
-
参数化延时函数
void Delay1ms(unsigned int xms) //@12.000MHZ { unsigned char i,j; while(xms) { i=2; //i和的迭代次数用STC软件自动生成出来 j=239; do { while(--j); while(--i); } xms--; } }
REGX52.H
头文件中有方法可以设置单个位
独立按键
- 开发板原理图
如原理图,单片机上电,所有IO口默认是高电平;没按下为高电平,按下为低电平。
- 通过按键控制LED:
#include <REGX52.H> void main() { while(1) { if(P3_1==0) { P2_0=0; } else { P2_0=1; } } }
- 防抖;利用延时代码可以消除抖动(按下按键后5 ~ 10ms再数据采集,收到松开按键信号后5 ~ 10ms 再去判断按键是否松开。)
#include <REGX52.H> //延时函数依旧可以用STC软件生成 void Delay(unsigned int xms) { unsigned char i,j; while(xms--) { i=2; j=239; do { while(--j); } while(--i); } } void main() { while(1) { if(P3_1=0) //==0表示按钮按下 { Delay(20); //按下时消抖 while(P3_1==0); //监控按下后是否松手,不松手则一直在当前while中循环不跳出去,直到松手程序才往下执行 Delay(20); //松开时消抖 P2_0=~P2_0; //取反操作,原来亮,现在就不亮。 } } }
- 左移右移
#include <REGX52.H> void Delay(unsigned int xms) { unsigned char i,j; while(xms--) { i=2; j=239; do { while(--j); } while(--i); } } void main() { usigned char LEDNum=0; while(1) { if(P3_1==0) { Delay(20); while(P3_1==0); Delay(20); LEDNum++; //控制左移 P2=~LEDNumm; //P2默认值是FF,直接左移P2没有意义 } } }
#include <REGX52.H> //延时函数 void Delay(unsigned int xms) { unsigned char i,j; while(xms--) { i=2; j=239; do { while(--j); } while(--i); } } sunsigned char LEDNum; //声明全局变量 //主函数 void main() { P2=~0x01; //初值00000001取反11111110 while(1) { if(P3_1==0) { Delay(20); while(P3_1==0); Delay(20); LEDNum++; if(LEDNum>=8) //实现循环左移 LEDNum=0; P2=~(0x01<<LEDNum); //左移 } //********************************// if(P3_0==0) { Delay(20); while(P3_0==0); Delay(20); if(LEDNum==0) LEDNum=7; //从最右开始 else LEDNum--; P2=~(0x01<<LEDNum); //右移 } } }
-
注意:C语言的自定义函数可以定义在main()前面,也可以定义在main()后面;当定义在main()后面,需要在main()前面声明,如:
void Delay(unsigned int xms);
把它加在main()前面。 -
演示:
数码管点亮了是因为138译码器上共用了P22/P23/P24
数码管
- 原理图
74HC138: 用输入(二进制,2的3次方)的三个(A/B/C)引脚实现控制输出(十进制,1..8)(Y0/Y1/Y2/Y3/Y4/Y5/Y6/Y7)8个引脚。
74HC245: 若DIR
接高电平,数据流向从A->B
;反之DIR
接低电平,数据流向从B->A
。
因为单片机的高电平驱动能力弱,低电平驱动能力强,所以有时单片机IO口的驱动用低电平驱动。
从原理图来看,开发板的数码管共阴极。
- 74HC138真值表
若要输出LED5,则P2_4(A2)=1,P2_3(A1)=0,P2_2(A0)=1。
E3对应原理图G1/E2对应原理图G2A/E1对应原理图G2B。
换算-> | 000:0 | 001: 1 | 010: 2 | 100: 4 | 011: 3 | 101: 5 | 110: 6 | 111: 7 |
-
用74HC138缺点:一次只能输出一个位选位(P2_2/3/4只能共同决定一个位)。实际上如果想要使数码管输出
看起来像
一次输出多个位选位,只需要把循环输出的延时调低
,骗过眼睛(动态显示数目管方式)。 -
数码管的共阴极/共阳极
上面是共阴极,下面是共阴极;判断标准:观察LED电流是从公共端流出还是流入公共端。
每个数码管的11/7/4/2/1/10/5/3一共8位被连接在了一起,控制12/9/8/6中的某个引脚接通,从而控制具体的显示内容。
-
共阴极,那么我们公共端(12、9、8、6)接地给0才会亮,及可以控制第几个亮;显示的数字则通过下面(11、7、4、2、1、10、5、3即对应字母ABCDEFG)控制,给1才亮,给0不亮;
- 段选(码):A/B/C/D/E/F/G
- 位选:12/9/8/6
- DP构成小数点
-
静态数码管显示方式:
#include <REGX52.H> main() { P2_4=1; //选中LED5-> "| 000:0 | 001: 1 | 010: 2 | 100: 4 | 011: 3 | 101: 5 | 110: 6 | 111: 7 | " P2_3=0; P2_2=1; P0=0x7D; //显示6,需要2#01111101 while(1){} }
#include <REGX52.H> //延时 void Delay(unsigned int xms) { unsigned char i,j; while(xms--) { i=2; j=239; do { while(--j); } while(--i); } } //共阴数码管段选 unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F}; //共阳数码管段选 unsigned char NixieTable_backup[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0xff}; void Nixie(unsigned char Location,Number) { switch(Location) { //共阴极 case 1:P2_4=1;P2_3=1;P2_2=1;break;//8 case 2:P2_4=1;P2_3=1;P2_2=0;break; case 3:P2_4=1;P2_3=0;P2_2=1;break; case 4:P2_4=1;P2_3=0;P2_2=0;break; case 5:P2_4=0;P2_3=1;P2_2=1;break; case 6:P2_4=0;P2_3=1;P2_2=0;break; case 7:P2_4=0;P2_3=0;P2_2=1;break; case 8:P2_4=0;P2_3=0;P2_2=0;break; } P0=NixieTable[Number]; } void main() { while(1) { //循环显示1-2-3 Nixie(1,1); Delay(200); Nixie(2,2); Delay(200); Nixie(3,3); Delay(200); } }
- 动态显示数码管方式:
- 动态显示数码管通常要
消影
,就是消除在其他位上的影子。因为数码管显示过程:位选 段选 位选 段选
。 在第一次段选完成之后和下一段选之后(下一位选还没选之前),下一位的段码是上一轮的段码 。通俗点语言讲就是,太快了,还忘不了前任。所以我们要做个上一轮的段码清零操作,清零之前记得加一个小延时让它稳定显示,立即清零会导致数码管比较暗。
- 动态显示数码管通常要
#include <REGX52.H> void Delay(unsigned int xms) { unsigned char i,j; while(xms--) { i=2; j=239; do { while(--j); } while(--i); } } unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F}; void Nixie(unsigned char Location,Number) { switch(Location) { //共阴极 case 1:P2_4=1;P2_3=1;P2_2=1;break;//8 case 2:P2_4=1;P2_3=1;P2_2=0;break; case 3:P2_4=1;P2_3=0;P2_2=1;break; case 4:P2_4=1;P2_3=0;P2_2=0;break; case 5:P2_4=0;P2_3=1;P2_2=1;break; case 6:P2_4=0;P2_3=1;P2_2=0;break; case 7:P2_4=0;P2_3=0;P2_2=1;break; case 8:P2_4=0;P2_3=0;P2_2=0;break; } P0=NixieTable[Number]; Delay(1); //消影 P0=0x00; //复位 } void main() { while(1) { Nixie(1,1); //Delay(200); Nixie(2,2); //Delay(200); Nixie(3,3); //Delay(200); } }
动态数码管显示如图
- 本次实验中,74HC138+74HC245的方式耗费了大量CPU的IO口(3+8),是比较浪费的。实际应用中,用专用数码管驱动芯片
TM1640
,内部自带显存,扫描电路。单片机只需要告诉他显示什么即可。可以节省很多IO口。
__EOF__