一,单片机及开发板介绍
1,基本介绍
- 单片机,英文Micro Controller Unit,简称MCU
- 内部集成了CPU、RAM、ROM、定时器、中断系统、通讯接口等一系列电脑的常用硬件功能
- 单片机的任务是信息采集(依靠传感器)、处理(依靠CPU)和硬件设备(例如电机,LED等)的控制
- 单片机跟计算机相比,单片机算是一个袖珍版计算机,一个芯片就能构成完整的计算机系统。但在性能上,与计算机相差甚远,但单片机成本低、体积小、结构简单,在生活和工业控制领域大有所用同时,学习使用单片机是了解计算机原理与结构的最佳选择
- 单片机上电时所有I/O口默认都为高电平
2,命名规则
以下为STC89C52系列的命名规则,不同系列单片机命名规则可能不同,如STC32G12K128351-LQPF64与其命名规则不同,具体的查STC官网手册
3,内部结构
单片机基本采用8051微处理器为内核,不同系列单片机不同主要是下图除8051微处理器外其他外设的不同,具体系列单片机的不同可以在STC官网查手册
4,最小应用系统
要想让单片机运行起来,需要给外部的一些电路,单片机和能让它运行起来的基本电路叫做最小应用系统,如下图所示,具体系列单片机的不同可以在STC官网查手册
右上为电源电路;左下为晶振电路(有一些单片机有内置晶振,可以不要此电路),为CPU提供时钟,驱动程序一步一步往下走;左中为复位电路,可以让程序从头开始运行,高电平时复位(上电一瞬间电容充电相当于短路,电路只连接上半部分将RST接为高电平,当电容充满时相当于断路,电流流向下半电路R1,此时RST为低电平,达到一个上电复位的效果) 。
二,点亮LED
1,点亮一个LED
下图所示,LED右端接VCC,左端如果为负极则导通,正极则不导通(单片机引脚输出高电平不导通,低电平导通)。
- CPU控制引脚高低电平的原理
MCU(单片机),内有许多个寄存器,寄存器就是存储器。寄存器以8个为一组(也就对应了引脚8个为一组,下图所示为P2系列引脚,每个存储器对应一个引脚),每个存储器都连接了一根线,再通过驱动器来增大电流,然后连接到引脚。CPU通过软件程序直接访问寄存器,给他里面写值,如果值为1,则寄存器输出高电平,如果为0,则寄存器输出低电平
要想让引脚P20输出低电平,就要通过代码实现
#include <REGX52.H>
//其中定义了各个寄存器的地址,这样P2就有了定义
//每个芯片的库是不一样的,右键点击Insert就可以引入该芯片的库
void main(){
P2=0xFE; //要使P20所连接的灯亮,其他灯都不亮,就要使寄存器配置为1111 1110
//单片机软件编程不支持二进制,所以就要将二进制转换为十六进制
}
//上述是一次操作了八位寄存器,也可以直接操作一位寄存器,写为P2_0=0;
//注:P2_0这种1位寄存器只在REGX52.H中有定义,在REG52.H中没有定义
2,LED闪烁
- 创建延时函数
第一步在STC-ISP中找到软件延时计算器并选择
第二步,修改系统频率为晶振频率。选择定时长度(即延时时间)。选择8051指令集,可以看又边框你选择的指令集适用于什么系列。
第三步
点击生成C代码,复制代码,然后回到Keil软件将代码粘贴,这样在使用的时候直接调用即可。
- 代码演示
#include <REGX52.H>
#include <INTRINS.H> #此头文件中定义了很多函数,其中包括_nop_()
void Delay500ms(void) //@12.000MHz
{
unsigned char data i, j, k;
_nop_();
i = 4;
j = 205;
k = 187;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
void main(){
while(1){
P2=0xFE;
Delay500ms();
P2=0xFF;
Delay500ms();
}
}
扩展:对延时函数进行修改,可以指定延时多少毫秒
void Delay1ms(int x) //@12.000MHz
{
unsigned char data i, j;
while(x){
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
x=x-1;
}
}
//在延时1ms的函数上进行修改
3,LED流水灯
#include <REGX52.H>
#include <INTRINS.H> #此头文件中定义了很多函数,其中包括_nop_()
void Delay500ms(void) //@12.000MHz
{
unsigned char data i, j, k;
_nop_();
i = 4;
j = 205;
k = 187;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
void main(){
while(1){
P2=0xFE; //1111 1110亮第一个
Delay500ms();
P2=0xFD; //1111 1101亮第二个
Delay500ms();
P2=0xFB; //1111 1011亮第三个
Delay500ms();
P2=0xF7; //1111 0111亮第四个
Delay500ms();
P2=0xEF; //1110 1111
Delay500ms();
P2=0xDF; //1101 1111
Delay500ms();
P2=0xBF; //1011 1111
Delay500ms();
P2=0x7F; //0111 1111
Delay500ms();
}
}
三,独立按键控制LED
轻触按键相当于电子开关,按下时接通,松开时断开
1,按下开关亮,松开就灭
#include <REGX52.H>
void main(){
while(1){
if(P3_1==0){//开关连接的I/O口为P3_1,单片机接通时,所有I/O口都为高电平。由于开关另一端接
//地,所以开关按下时,P3_1输出低电平。
P2_0=0;//点亮第一个LED灯
}else{
P2_0=1;
}
}
}
2,按一下开关亮,松开后仍亮,再按一次灭
对于机械开关,当机械触电断开,闭合时,由于机械触点的弹性作用,一个开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开,所以在开关闭合及断开的瞬间会伴随一连串的抖动。为了解决这个问题,可以在按下时延时几十毫秒,松开时也延时几十毫秒。
#include <REGX52.H>
#include <INTRINS.H>
void Delay1ms(int x) //@12.000MHz
{
unsigned char data i, j;
while(x){
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
x=x-1;
}
}
void mian(){
while(1){
if(P3_1==0){
Delay1ms(20);
while(P3_1==0);
Delay1ms(20);
P2_0=~P2_0;//位运算取反,可以让P2_0亮或者灭
}
}
}
3,LED按照1~9的二进制形式依次闪烁
#include <REGX52.H>
#include <INTRINS.H>
void Delay1ms(int x) //@12.000MHz
{
unsigned char data i, j;
while(x){
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
x=x-1;
}
}
void mian(){
unsigned int LEDNum=0;//设置一个变量表示数字1~9,因为寄存器8个为一组,而char为8个字符,所
//以用char
while(1){
if(P3_1==0){
Delay1ms(20);
while(P3_1==0);
Delay1ms(20);
LEDNum++; //LEDNum不断增加,即依次表示1~9
P2=~LEDNum;//当单片机通电后所有引脚默认为高电平1,根据点亮LED工程中所说的
//二极管负极接引脚,所以当引脚输出低电平时二极管才能点亮
}
}
}
//P2++:因为所有引脚默认为高电平,所以为1111 1111,P2++超出范围变为0000 0000,
//再P2++,变为1111 1111
4,LED依次点亮(按一下开关移位一次)
#include <REGX52.H>
#include <INTRINS.H>
void Delay1ms(int x) //@12.000MHz
{
unsigned char data i, j;
while(x){
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
x=x-1;
}
}
void mian(){
unsigned int LEDNum=0;//设置一个变量表示数字1~9
while(1){
if(P3_1==0){
Delay1ms(20);
while(P3_1==0);
Delay1ms(20);
LEDNum++;
if(LEDNum>=8) LEDNum=0;
P2=~(0x00<<LEDNum);//每次左移一位
}
}
}
四,数码管
1,数码管介绍
(1)一位LED数码管
一位数码管共有8个LED。其内部连接如下图,为了减少数码管的引脚数,在数码管内将8个LED的正极或负极引脚连接起来,接成一个公共端(COM端),根据公共端是正极还是负极,可分为共阳极和共阴极。数码管共十个引脚,左下角的为1号引脚,逆时针递增,中间的两个引脚(即3号和8号引脚为两个公共端)。
(2)多位LED数码管
如图为4位数码管,他有两排12个引脚。内部电路如图所示,也分为共阳极和共阴极两种方式。控制方式:比如说要控制第二个数码管显示数字1(共阳极),我们就可以使9号引脚为高电平,再令11号引脚为低电平(假如11号引脚控制的是最右段LED),这样就可以使第二个数码管显示一,这叫做静态显示。但是这种方法只能让单个数码管显示或者多个数码管显示同一个数字,为了让不同数码管显示不同数字,我们就要采用动态显示的方法,利用人眼的暂留特性,先显示第一位,再快速显示第二位,这样就看起来好像两位在同时点亮。
2,数码管驱动器件
下图所示为单片机学习板数码管部分的原理图
- 138译码器
P22,P23,P24 接单片机I/O口,引脚Y0~Y7分别接8个数码管的8个公共端,此译码器的作用是减少单片机I/O口的占用,将8个引脚转为P22,P23,P24这3个引脚控制(所以叫138译码器)。原理:二进制转化。P24,P23,P22(C为最高位,也就是P24为最高位)可以表示二进制111,转化为十进制就是7,而P24,P23,P22为000时转为十进制就为0,所以P24,P23,P22可以表示0~7这8个数,也就对应了Y0~Y7。当CBA分别为001时就对应了Y1为0,其他都为1;当CBA为011时就代表Y3为0,其他都为1。
左下角的G1,G2A,G2B叫做使能端,相当于一种电源开关。G1接高电平,G2A,G2B接上低电平,这个芯片就能工作。除此之外,这个芯片还需要电源和接地。
- 双向数据缓冲器
VCC和GND为电源。OE是这个芯片的使能,如图所示它连接了低电平所以这个芯片可以工作(低电平有效)。DIR(direction)引脚,也就是方向的意思,主要用于控制将左边数据缓冲到右边还是右边数据缓冲到左边(如果DIR接高电平就是从左到右,如果接低电平就是从右往左),如图所示,它接LE引脚,LE为跳线帽,它插在哪个地方就把两个引脚给短路,实物图中将LE插到VCC,所以此缓冲器从左往右缓冲。A0连接B0,A1连接B1,A2连接B2,依次类推,它就是起一个数据缓冲的作用。
需要缓冲的原因:因为单片机高电平驱动能力有限,输出最大电流不能太大 ,低电平驱动能力强一些(所以LED通常采用低电平点亮)。所以如果没有缓冲器直接与单片机连接,它的电流会很小,灯会暗。加上缓冲器后可以提高驱动能力,单片机的高电平会作为信号(只需要微弱的信号即可被缓冲器接收到)进入缓冲器,然后缓冲器用自己的电源为数码管提供高电平
3,静态数码管显示
#include <REGX52.H>
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x39,0x5E,0x79,0x71,0x00};//分别对应0~9,A,B,C,D,E,F,空
void Nixie(unsigned char Location, Number){
switch(Location)//Location代表哪个数码管显示
{
case 1:P2_4=1;P2_3=1;P2_2=1;break;
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){
Nixie(3,2);//第3个晶体管显示2
}
}
4,动态数码管显示
在操作数码管时,如果仅是下方代码会出现下图所示的数码管重影现象,这是因为数码管显示时会有一个位选和段选的过程(一般地,操作数码管时,先执行段选再执行位选。位选是选择待操作的数码管,如开发板上的是8位数码管,位选就是选择8位数码管中的某一个。段选是选择数码管里面的LED灯,即通过选择点亮响应的LED灯以达到显示需要的数据的目的。)但是由于单片机速度很快,位选-段选-位选-段选————的过程就会变为段选-位选-段选-位选的过程(再选中下一位时上一位还没有完全消失直接串到下一位,导致重影)
#include <REGX52.H>
void Delay1ms(int x) //@12.000MHz
{
unsigned char data i, j;
while(x){
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
x=x-1;
}
}
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x39,0x5E,0x79,0x71,0x00};//分别对应0~9,A,B,C,D,E,F,空
void Nixie(unsigned char Location, Number){
switch(Location)//Location代表哪个数码管显示
{
case 1:P2_4=1;P2_3=1;P2_2=1;break;
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){
Nixie(3,2);//第3个晶体管显示2
Nixie(2,4);
Nixie(1,5);
}
}
为了避免这个问题,我们就需要在段选之后把它清零 ,以下为正确代码
#include <REGX52.H>
void Delay1ms(int x) //@12.000MHz
{
unsigned char data i, j;
while(x){
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
x=x-1;
}
}
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x39,0x5E,0x79,0x71,0x00};//分别对应0~9,A,B,C,D,E,F,空
void Nixie(unsigned char Location, Number){
switch(Location)//Location代表哪个数码管显示
{
case 1:P2_4=1;P2_3=1;P2_2=1;break;
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(3,2);//第3个晶体管显示2
Nixie(2,4);
Nixie(1,5);
}
}
- 补充
上述方法属于单片机直接扫描的方法,就是不断给单片机输入要显示的数据。这种方法对硬件设备要求简单,但会耗费大量CPU时间。
专用驱动芯片扫描的方法:其内部自带显存,扫描电路,单片机只需要按照通讯协议告诉它显示什么即可 (TM1640专用驱动芯片扫描)(74HC595三根线即可驱动)
五,LCD1602调试工具
使用LCD1602液晶屏作为调试窗口,提供类似printf函数的功能,可实时观察单片机内部数据的变换情况,便于调试和演示。也可使用串口进行调试
这里提供的LCD1602代码属于模块化的代码,只需要知道这个函数的作用和使用方法即可。这些代码是需要自己编写的,有具体的.c文件,.h文件
使用之前必须先初始化
#include <REGX52.H>
#include "LCD1602.H"
void main(){
LCD_Init();
LCD_ShowChar(1,1,'A');//在第一行第一列显示A
LCD_ShowString(1,3,"Hello");//在第一行第三列开始显示Hello
LCD_ShowNum(1,9,123,3);//在第一行第九咧显示123,显示位数为3
LCD_ShowSignedNum(1,13,-66);
}
六,矩阵键盘
在键盘中按键数量较多时,为了减少I/O口的占用,通常将按键排列成矩阵形式。采用逐行或逐列的扫描,就可以读出任何按键的状态。数码管的扫描也是矩阵的形式,它是输出扫描,矩阵键盘属于输入扫描。下图为矩阵键盘的原理图
矩阵按键读取过程:如上图,如果给P10低电平0,读取P17,P16,P15,P14的引脚状态,如果P17输入为低电平,则表示按键S4按下,如果P16输入为低电平,则表示S8按下。
单片机的I/O口为一种弱上拉模式,又叫准双向口,这使得一个I/O口既可以输入,又可以输出。为什么上述按键读取过程中,P17接触了低电平P10后读取为低电平,而不是他本身输出的高电平?这是因为单片机弱上拉模式的内部结构简单图为下图,当要输出高电平时就把开关接到VCC,要输出低电平就把开关接到地;读取时在图中节点引出的电路,然后经过施密特触发器等后续电路来读取。这样的话如果I/O口接地,那么读取的就是接地的低电平0,而不会受输出高电平的影响。
1,利用LCD1602显示按下的矩阵键盘的键码值
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
int MatrixKey(){
int KeyNumber=0;
P1=0xFF;//将P1的I/O口全部置为1
P1_3=0;
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=1;}//按键检测
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=5;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=9;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=13;}
P1=0xFF;//将P1的I/O口全部置为1
P1_2=0;
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=2;}//按键检测
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=6;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=10;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=14;}
P1=0xFF;//将P1的I/O口全部置为1
P1_1=0;
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=3;}//按键检测
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=7;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=11;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=15;}
P1=0xFF;//将P1的I/O口全部置为1
P1_0=0;
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=4;}//按键检测
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=8;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=12;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=16;}
return KeyNumber;
}
void main(){
LCD_Init();
while(1){
int a=MatrixKey();
if(a){
LCD_ShowNum(1,1,a,2);//在第一行第九咧显示123,显示位数为3
}
}
}
2,矩阵键盘密码锁
S1~S9表示数字1~9,S10表示数字0,S11为确定,S12为取消
七,定时器(计数器)
1,定时器介绍
定时器属于单片机内部资源,其电路的连接和运转均在单片机内部完成。
定时器作用:1,用于计时系统,可实现软件计时,或是程序每隔一段时间完成一项操作2,替代长时间的延时函数,提高CPU运行效率和处理速度(延时函数会使CPU进入等待,直到延时时间结束CPU才开始重新运行,这就说明CPU在延时的这段时间内无法完成其他事情。而利用定时器来延时的话,CPU就可以做其他事情。)
STC89C52中有3个定时器T0,T1,T2,T0和T1与传统的51单片机兼容,T2是此型号单片机增加的资源(不同型号的单片机定时器的个数和操作方式有所不同,但一般T0,T1的操作方式是51单片机所共有的)
2,定时器原理
(1)运行框图
定时器在单片机内部相当于一个闹钟,根据时钟的输出信号,每来一次脉冲计数单元就增加一,当计数单元数值增加到“设定的闹钟提醒时间”时,计数单元就会向中断系统发出中断申请,产生闹铃提醒,使程序跳转到中断服务函数中执行
(2)定时器内部模式及模式1原理
- 工作模式:
STC89C52的T0和T1均有四种工作模式:
模式0:13位定时器/计数器;模式1:16位定时器/计数器(最常用);模式2:8位自动重装模式;模式3:两个8位计数器
- 模式1原理:
如下图所示,其内部电路按照运行框图共分为三部分。
- 计数器
可以看到计数器中的TL1(timer low)和TH1(timer high),他就是一个16位的计数器(由两个字节组成,高字节为TH,低字节为TL。1表示为定时器T1),2个字节最大能计数65535。左边的时钟为计数器提供脉冲,每来一个脉冲计数器就加一。当加到最大的65535时,再来一个脉冲计数器就会归0(当计数器达到最大值时,给TF1一个标志位,然后传给中断系统产生中断)。计数器下方为它的控制位,可以控制计数器启动或者暂停
- 时钟
脉冲的时间由SYSclk来确定,它是系统时钟,即晶振周期(STC89C52晶振频率为12MHz)。时钟有两个来源,一个是SYSclk,一个是T0 Pin,T0 Pin是单片机上的一个引脚,也就是说单片机的时钟可以由单片机自己提供,也可以由引脚连接的外设提供,当由外设提供时,定时器就变成了一个计数器。SYSclk提供晶振频率后,会经过分频(如上图电路),+12表示12分频,输出的就为1MHz,表示一次为1微秒记一次数,当记到最大值就会产生中断;+6表示6分频,每个2微秒记一次数。电路之后的C/T表示开关,如果给高电平1,那么开关就会连接到T0 Pin,如果给低电平0,开关就会连接到SYSclk。
- 中断系统
中断资源适合单片机型号有关的,不同型号的单片机拥有的中断资源不同,例如中断源个数不同,中断优先级个数不同等。
STC89C52中有8个中断源(外部中断0,定时器0中断,外部中断1,定时器1中断,串口中断,外部中断2,外部中断3) ,4个中断优先级
3,定时器寄存器
(1)寄存器作用介绍
让中断按照我们想要的方式运行,就要依靠定时相关寄存器。单片机中寄存器就是一种特殊的RAM,一方面它可以存储和读取数据,另一方面,每个寄存器背后都连接了一根导线,控制着电路的连接方式。寄存器相当于一个复杂机器的操作按钮(单片机通过寄存器配置内部线路的连接)
寄存器就是用来控制下图电路中的开关,如C/T寄存器可以控制C/T开关拨到那个位置
(3)定时器/计数器控制寄存器TCON(timer control)
以下为官方手册上的内容
可以结合电路上的寄存器进行理解
(3)定时器/计数器工作模式寄存器 (timer mode)
- GATE寄存器
GATE:计数器的启动暂停可以由控制寄存器的TR1直接控制,也可以由TR0和外设INT1来联合控制(INT1为单片机引脚)。GATE就可以选择是TR1单独控制还是联合控制
控制原理:可以看下图电路GATE经过非门,然后经过或门,最后通过与门。如果GATE置0,那么经过非门信号就变为1,由于是或门,有1就为1,所以不管INT1是0还是1,或门输出的都为1,这时就不受INT1控制;如果GATE为1,经过非门为0,这时INT1为1则输出1,为0则输出0,也就是受INT1控制
- 其他寄存器
(4)TH,TL寄存器
前面说过,来计数的寄存器
4,中断寄存器
(1) 中断允许寄存器IE和XICON
EA为总中断允许控制位,EA=0所有的中断都会关闭,如下简化图,EA相当于总开关
其他的如ET2,ES,ET1,EX1,ET0,EX0就是控制单条路的开关
(2)中断优先级控制寄存器IP/XICON和IPH
5,程序实现定时器计时
- 配置定时器模式TMOD:
配置定时器模式TMOD(启动定时器0,并且定时器0处于16位定时器模式,根据前面的表格,可知M1=0,M0=1时定时器0处于16位定时器模式),即TMOD=0000 0001,换算为16进制为0x01。注意:这里的TMOD上写着不可位寻址表示的是只能整体赋值,而TCON上写着可位寻址则表示可以给单独一个寄存器为1或者0.
- 配置TCON:
然后根据下表配置TCON。着重说一下配置计数器的寄存器TL0和TH0,我们知道这个寄存器能记录0~65535的次数,当达到65535时就请求中断,在STC89C52中晶振频率12MHz,12分频后也就是每隔1微秒计数加1,总共可以定时65535微秒,也就65毫秒左右。如果想让它定时1s,我们可以先让他记满1毫秒产生中断,然后再记1毫秒,这样记1000次就可以达到计数1s的目的。为了让他一次能记1ms,需要将它初始化为64535
- 配置中断
按照电路图配置即可
- 中断函数
中断函数(即达到规定时间后,需要CPU做什么),这里我们用的定时器0,就需要使用C这个函数.中断函数中进行简短的任务,执行的任务时间不能太长
- 最终代码
#include <REGX52.H>
void Timer0_Init(){
TMOD=0x01;//配置模式0000 0001
//如果使用两个定时器时,配置第一个而不影响第二个,配置第二个而不影响第一个,
//就可以使用与或的方法,如配置定时器0而不影响定时器1 TMOD=TMOD&0xF0 TMOD=TMOD|0x01
//第一步与使得高四位不变,低四位清零,第二步或可以使高四位不变,
//低四位按要求改变(这里是将低四位置为0001)
TF0=0;//中断溢出标志位要先清0
TR0=1;//开启定时器0
TH0=64535/256;
TL0=64535%256;//或者用程序员计算器计算65532的十六进制
ET0=1;//中断配置
EA=1;
PT0=0;
}
void main(){
Timer0_Init();
while(1)
{
}
}
int T0_Count;//计数变量,当其为1000时说明记了1000个1ms,即1s
void Timer0_Rountine(void) interrupt 1
{
TH0=64535/256;
TL0=64535%256;//每次记1ms后都赋初值,使其从64535开始记
T0_Count++;
if(T0_Count>=1000){
T0_Count=0;
//加上需要执行的操作
}
}
也可以使用STC-ISP来自动配置定时器 如下图
再定时器计算器中选择好时钟频率,定时长度(不能太长),定时器模式(根据所需要的定时器模式选择),定时器时钟(根据定时器原理图可知为12T)
复制过来后要做修改,比如STC89C52没有AUXR配置12T模式,而是系统模式已经配置好了(新版本有AUXR配置模式),所以第一行删除。除此之外,此代码没有中断的配置,需要自己加上。
6,应用1:按键控制LED流水灯&定时器
介绍两个函数,这两个函数包含在函数库#Include <INTRINS.H>中
unsigned char _cror_(unsigned char, usigned char); 和 unsigned char _crol_(unsigned char, usigned char);分别指循环右移和循环左移。第一个形参表示要移位的数值,第二个参数表示移多少位 比如
unsigned char a=0x01;
a=_cror_(a,1);//这时a等于0x02
a=_cror_(a,2);//这时a等于0x04
//这样看它和<<左移没什么区别。
//但它们的区别是当a移到最高位0x80时他就会回到最开始的0x01,以此来循环移位
//_crol_同理
#include <REGX52.H>
#include "Timer0.h"
#include "Key.h"
#include <INTRINS.H>
unsigned char KeyNum,LEDMode;
void main()
{
P2=0xFE;//先将LED等的最后一位点亮
Timer0Init();//定时器初始化
while(1)
{
KeyNum=Key(); //获取独立按键键码
if(KeyNum) //如果按键按下
{
if(KeyNum==1) //如果K1按键按下
{
LEDMode++; //模式切换
if(LEDMode>=2)LEDMode=0;
}
}
}
}
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count;
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++; //T0Count计次,对中断频率进行分频
if(T0Count>=500)//分频500次,500ms
{
T0Count=0;
if(LEDMode==0) //模式判断
P2=_crol_(P2,1); //LED输出
if(LEDMode==1)
P2=_cror_(P2,1);
}
}
//自写版本
#include <REGX52.H>
#include "LCD1602.h"
#include "Key.h"
#include "Timer0.h"
unsigned char KeyNum=0,LEDMode=0;
int time_count=0;
int a=0x01;
void main(){
Timer0_Init();
while(1){
KeyNum=Key();
if(KeyNum==1)
{
LEDMode++;
if(LEDMode>=3)
{
LEDMode=1;
}
}
}
}
void Timer0_Routine() interrupt 1
{
TL0=0x17;
TH0=0xFC;
time_count++;
if(time_count>=500){
time_count=0;
if(LEDMode==1)
{ if(a>128) a=1;
P2=~a;
a=a<<1;
}
if(LEDMode==2)
{
if(a<1) a=128;
P2=~a;
a=a>>1;
}
}
}
7,应用2:定时器时钟
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "Timer0.h"
unsigned char Sec=55,Min=59,Hour=23;
void main()
{
LCD_Init();
Timer0Init();
LCD_ShowString(1,1,"Clock:"); //上电显示静态字符串
LCD_ShowString(2,1," : :");
while(1)
{
LCD_ShowNum(2,1,Hour,2); //显示时分秒
LCD_ShowNum(2,4,Min,2);
LCD_ShowNum(2,7,Sec,2);
}
}
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count;
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++;
if(T0Count>=1000) //定时器分频,1s
{
T0Count=0;
Sec++; //1秒到,Sec自增
if(Sec>=60)
{
Sec=0; //60秒到,Sec清0,Min自增
Min++;
if(Min>=60)
{
Min=0; //60分钟到,Min清0,Hour自增
Hour++;
if(Hour>=24)
{
Hour=0; //24小时到,Hour清0
}
}
}
}
}
//自写版本
#include <REGX52.H>
#include "LCD1602.h"
#include "Key.h"
#include "Timer0.h"
#include "Delay.h"
unsigned char KeyNum=0,LEDMode=0;
int time_count=0;
int s,h,m;
void main(){
Timer0_Init();
LCD_Init();
LCD_ShowString(1,1,"CLOCK:");
LCD_ShowString(2,1," : : ");
while(1){
LCD_ShowNum(2,1,h,2);
LCD_ShowNum(2,4,m,2);
LCD_ShowNum(2,7,s,2);
}
}
void Timer0_Routine() interrupt 1
{
TL0=0x17;
TH0=0xFC;
time_count++;
if(time_count>=1000){
time_count=0;
s++;
if(s>=60){m++;s=0;}
if(m>=60){h++;m=0;}
}
}
八,串口通信
1,串口理论知识介绍
(1)串口基本介绍
串口是一种应用十分广泛的通讯接口,可实现两个设备的互相通信。单片机的串口可以实现单片机与单片机,单片机与电脑,单片机与各式各样的模块互相通信。
51单片机内部自带UART,可实现单片机的串口通信
(2)串口数据发送(STC-ISP)
用单片机给电脑发送数据时,可以用STC-ISP中的串口助手的接收缓冲区查看。也可以用电脑像单片机发送数据以实现某些功能,在发送缓冲区输入要发送的数据
(3)串口连接方式
简单的双向串口通信有两根通信线(发送端TXD和接收端RXD),TXD和RXD要交叉连接(如下图)。当只需要单向传输数据时,可以直接用一根通信线。此外,复杂的串口会有很多通讯接口,如D89母头
注意:当电平标准不一致时,需要使用电平转换芯片(电平标准如下)
- 电平标准:
电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压和数据的对应关系,串口常用的电平标准有三种 :
TTL电平:+5V表示1,0V表示0
RS232电平:-15~-3V表示1,+3~+15V表示0
RS485电平:两线压差+2~+6V表示1,-2~-6V表示0(差分信号)
(4)常见串口比较
点对点通信:指两个设备之间进行通信。
CAN主要用于汽车领域,使用差分信号,传输距离远,稳定性好
异步,同步进一步解释:通信双方发送数据时,比如A发10(高低电平),B收到的是10(高低电平)。A再发1100,B如果接受的速率不同,就可能会接收成10(A发送1个A用时1ms,而B为2ms接收一次,这样B接受的就变为1),所以要保证接受速率相同,比如A每隔1ms发送一次那么B就要每隔1ms接受一次,这就是异步,双方规定一个相同的速率发送和接收。同步是通过一根时钟线,只检测这一根线,当A给一个上升沿(高电平),B就采样一次,这样来达到目的,同步用于对时间要求严格,它可以做到同时发送与接收。可以看一下上面不同的串口,有同步的引脚都需要SCL或SCLK这样的时钟线
总线进一步理解:可挂载多个设备的都用到总线(可以用下图理解)
(5)STC89C52内部串口
(6)串口参数和时序图
波特率:串口通信的速率(发送和接收数据位的间隔时间)
校验位:用于数据检验,比如STC89C52的8位UART和9位UART,8位UART表示一个字节,9位UART就是一个字节加上最后一位校验位,可以检验前面数据的正确性。常用的是奇偶校验,以奇校验为例,双方约定了使用奇校验,A发送0000 0011这个是1个数为偶数,所以再加一位1,发送数据为0000 0011 1,B接收时,如果1的个数变为偶数,则表示数据错误;如果A发送为0000 0001,则再加一位0,发送数据1为0000 0001 0,B接收时,如果1的个数变为偶数,则表示数据错误。但是如果两个数据同时反转,1还为奇数,这样就检测不出来,所以这种方法准确率并不高。
停止位:用于数据帧间隔(一个数据发送完后停止一小段时间)。数据是一个一个发送的,比如一个字节有8个位,数据就是1位1位的发送,先发低位,再发高位,接收也是一位一位的收
(7)串口模式图
下图所示。数据只有到达单片机的总线,单片机才能接收处理这些数据。红框内的电路是用来控制波特率,也就是说通信双方的速率主要是由定时器来约定的,TH1和TL1是定时器寄存器,通过溢出率经2分频,16分频等来控制收发器的采样时间,使用时配置T1定时器的TH1,TL1来控制收发速率
SBUF:串口数据缓存寄存器,物理上是两个独立的寄存器,但占用相同的地址。写操作时,写入的是发送寄存器,读操作时,读出的是接收寄存器。
发送时先将数据写入SBUF中,再通过发送控制器的控制将数据发送出去;接收时,从RXD接收数据回来经过移位寄存器一位位的移到SBUF中,然后需要数据时直接读缓存就好了。写程序时,当SBUF在等号左边,意味着给SBUF赋值,也就是要发送的数据写入SBUF;如果SBUF在等号右边,意味着取SBUF的值,也就是要读取SBUF存的数据。当收到一位数据时,接收控制器就会产生一个叫RI的接收中断,提示可以取数据了;发送数据时,数据发送完发送控制器会产生一个叫TI的中断,提示发送数据完毕
加上去中断逻辑的电路图如下
单片机通过如下电路通过USB与电脑连接
2,STC89C52的串行口寄存器
3,单片机向电脑发送数据
(1)配置寄存器(串口初始化)
- 配置SCON
SCON是串行控制寄存器,用于选择串行通信的工作方式和某些控制功能。PCON是波特率选择特殊功能寄存器
这里我们使用串口通讯的模式二,下面是软件实现方式。在PCON中对SMOD和SMOD0的描述(下图)可以得知SCON中的SM0/FE有两个作用,当SMOD0变化时,这两个作用切换,当SMOD0=0时,处于SM0模式,此时它和SM1搭配就可以配出四种模式。令SM0=0,SM1=1就可以使串口处于模式一。
接着配置SCON的其他寄存器。如下图,SM2用来控制方式2和3的,我们使用的是方式1,所以此位置为0;REN位串行接收控制位的开关,当需要单片机接收数据时我们置为1;TB8和RB8均为方式2和3的,置为0;TI和RI分别为发送和接收的中断请求标志位,我们在模式图中看过,注意,当数据发送或接收完毕后,它是由系统置为1,需要我们用软件重新置为0。
所以总结来说SCON应用二进制表示为0100 0000.由于SCON可以位寻址,所以可以直接用SM1=1等单独表示
- 配置PCON
波特率加倍可以减少误差(可以参照下方配置定时器内容),所以SMOD置为1.PCON配置为1000 0000.即PCON |=0x80
- 配置SBUF
SBUF用于往里面写数据或者从里面取数据,初始化阶段不需要配置
- 配置定时器
根据下图可以知道控制波特率需要先配置定时器,图中是定时器T1。串口定时器需要使用8位自动重装模式,原来的16位不自动重装模式并不精准,而波特率变化是很快的,所以我们需要更精准的8位自动重装模式
配置为模式8位自动重装,则TMOD=0010 0000.则写为TMOD &=0x0F; (前四位清零)TMOD |= 0x20;(配置前四位为0010)
利用STC-ISP配置定时器和波特率,注意勾选波特率倍速,勾选波特率倍速可以降低误差。这里的定时器只做溢出波特率发生器(只要有溢出即可)(参照模式图),而不进入中断,所以这里会关闭中断
(2)发送数据函数编写
#include <REGX.H>
#include "Dalay.h"
void Uart1_Init(void) //4800bps@12.000MHz
{
PCON |= 0x80; //使能波特率倍速位SMOD
SCON = 0x40; //8位数据,可变波特率
TMOD &= 0x0F; //设置定时器模式
TMOD |= 0x20; //设置定时器模式
TL1 = 0xF3; //设置定时初始值
TH1 = 0xF3; //设置定时重载值
ET1 = 0; //禁止定时器中断
TR1 = 1; //定时器1开始计时
}
void UART_SendByte(unsigned char Byte)
{
SBUF=Byte;
while(TI==0);
TI=0;//软件复位标志位
}
void main(){
Uart1_Init();
UART_SendByte(0x66);
while(1){
//如果把UART_SendByte(0x66);放到while里面,让单片机一直发数据
//电脑接收的数据就会出现问题,这是因为误差的原因(整数晶振频率存在误差)
//小数晶振频率如11.0592MHz误差就可以达到0。对于整数晶振频率如本例所用的
//12MHz就会存在误差,所以可以在每次UART_SendByte(0x66);完后加一个delay(10)
//还有一点就是可以降低波特率,波特率越低数据越稳定
}
}
接收数据时一定要把波特率调为发送数据设备的波特率,这里单片机发送数据为4800,接收时波特率也要调味4800. 校验位一般设置为无校验,停止位设为1位
文本模式,HEX模式:HEX模式是十六进制模式,比如接收缓冲区为HEX模式时,单片机发送0x23,则接收缓冲区接收为23.如果接收缓冲区为文本模式,则根据ASCⅡ表将0x23接收为C。当然,单片机发送的数据也可以是字符‘A’等。
4,电脑向单片机发送数据
电脑通过串口控制LED:
单片机接收数据时需要使用中断函数,因为不知道电脑何时会发数据,所以利用中断系统每当发过来数据就接收它。
(1)配置寄存器
在单片机向电脑发送数据时说过SCON寄存器,其中有一个REN,当要接收数据时就要将这个REN置为1。和前面一样的模式,SM0=0,SM1=1,其他位为0,所以SCON=0x50
配置中断:EA=1(打开中断总开关),ES=1(打开串口中断开关)
配置完成后,当中断来时跳转到中断函数 ,下面的UART_Rountine就是所需要的中断函数
(2)代码实现
以下代码可以实现电脑发送数据控制LED,而且还可以将单片机接收的数据再发给电脑
#include <REGX.H>
#include "Dalay.h"
void Uart1_Init(void) //4800bps@12.000MHz
{
PCON |= 0x80; //使能波特率倍速位SMOD
SCON = 0x50; //8位数据,可变波特率
TMOD &= 0x0F; //设置定时器模式
TMOD |= 0x20; //设置定时器模式
TL1 = 0x64; //设置定时初始值
TH1 = 0x64; //设置定时重载值
ET1 = 0; //禁止定时器中断
TR1 = 1; //定时器1开始计时
EA=1;
ES=1;
}
void UART_SendByte(unsigned char Byte)
{
SBUF=byte;
while(TI==0);
TI=0;//软件复位标志位
}
void UART_Routine() interrupt 4
{
if(RI==1)//因为接收,发送都会触发中断,这里保证是单片机接收数据中断
{
P2=SBUF;
UART_SendByte(SBUF);//注意要写在中断函数,不能再写在主函数
RI=0;
}
}
void main(){
Uart1_Init();
UART_SendByte(0x66);
while(1){
}
}
九,LED点阵屏
1,点阵屏介绍
LED点阵屏由若干个独立的LED组成,LED以矩阵的形式排列,以灯的亮灭来显示图像。
LED点阵屏按颜色分为单色,双色,全彩(每个LED中由红绿蓝三个LED)。大规模LED点阵通常由很多个小点阵拼接而成。
2,显示原理
LED点阵屏结构类似于数码管,有共阴和共阳两种接法。LED点阵屏需要进行逐行或逐列扫描才能使所有LED同时显示
3,单片机控制LED点阵屏原理
LED点阵屏有16个引脚接口,如果全部连接在单片机上就会浪费单片机引脚资源,所以使用74HC595来扩展引脚(如下图)
74HC595工作原理:
74HC595是串行输入并行输出的移位寄存器,可用三根线输入串行数据,8根线输出并行数据,多片级联后,还可输出16为,24位,32位等,常用于IO口扩展。
它的左端P35,P36,P34接单片机引脚,也就是通过3个单片机引脚控制LED点阵屏8个引脚D0~D7。 OE是输出使能,其上加了横线表示低电平有效,也就是OE引脚接低电平这个芯片才有效。SRCLR叫做串行清零端,会把数据进行清空,其上加了横线表示低电平时才会清空,原理图中SRCLR接VCC表示不清空。QH‘用于多片级联。SER表示串行数据(数据分为串行数据和并行数据,串行就是数据根据时钟一个一个的发送,并行就是同时给多个数据,比如同时给D0~D7八个引脚数据)
SRCLK为串行时钟,当时钟每来一个上升沿,SER串行数据就输入一个数据到移位寄存器(如下图左为移位寄存器,右为缓存器)。当RCLK来一个上升沿时,就会把移位寄存器内的八个数据同时送到缓存器。比如,要给QA~QH输出0000 0101,由于单片机上电后默认IO口为高电平,所以开始要给SRCLK初始化为低电平,将RCLK也初始化为低电平,SER串行口输入数据需要先填到QH的移位寄存器,也就是先给SER写1,当给SER写1时,把SRCLK设置为高电平,这时1就到了移位寄存器的第一个格,再对SRCLK清零和SER清零,下一个数据SER输入为0,再给SRCLK输入高电平,0就到了刚才1的位置,1往下移动一格。不断输入数据,当输入8个数据后,设置RCLK为高电平,数据就会从移位寄存器搬到缓存器再进行输出。如果想要多片联结输出多位,就再按相同的步骤给SER数据然后放到移位寄存器,这时移位寄存器的数据溢出,溢出的数据就会通过QH'到下一个移位寄存器
理论来说,如果将点阵屏的16个引脚全都接在单片机的IO口上是可行的,但是单片机的IO口是弱上拉模式,在驱动高频电路时,它的高电平输出电流很低,这会导致点阵屏很暗。如果在单片机IO口后接一个三极管开关再驱动点阵屏就可以实现,如下电路图,如果给I/O口低电平,三极管就会导通,电压VCC就会直接去驱动引脚,而不是I/O电压驱动引脚,这里I/O相当于一个控制信号。
4,应用1:显示静态图像
- 先介绍几个概念
•可位寻址/不可位寻址:在单片机系统中,操作任意寄存器或者某一位的数据时,必须给出其物理地址,又因为一个寄存器里有8位,所以位的数量是寄存器数量的8倍,单片机无法对所有位进行编码,故每8个寄存器中,只有一个是可以位寻址的。对不可位寻址的寄存器,若要只操作其中一位而不影响其它位时,可用“&=”、“|=”、“^=”的方法进行位操作
•sfr(special function register):特殊功能寄存器声明。例:sfr P0 = 0x80;声明P0口寄存器,物理地址为0x80
•sbit(special bit):特殊位声明。例:sbit P0_1 = 0x81; 或 sbit P0_1 = P0^1;声明P0寄存器的第1位
sfr ,sbit用在头文件的声明中,比如STC89C52的头文件<REGX52.H>
有了以上概念,我们在操作74HC595时为了方便,就可以把引脚P3_5重新定义为以RCLK为名的引脚,如下方
#include <REGX52.H>
sbit RCK=P3^5;//P3_5是P3系列引脚的第五位,重新定义时直接用P3^5
//就可以表示为P3_5的引脚地址0xB5,由于RCLK已经命名过了
//所以命名为RCK
sbit SRCLK=P3^6;
sbit SER=P3^4;
#include <REGX52.H>
#include <Delay.h>
sbit RCK=P3^5;//P3_5是P3系列引脚的第五位,重新定义时直接用P3^5
//就可以表示为P3_5的引脚地址0xB5,由于RCLK已经命名过了
//所以命名为RCK
sbit SRCLK=P3^6;
sbit SER=P3^4;
int i=0;
void _74HC595_WriteByte(unsigned char Byte){//此函数用于74HC595传输数据
for(i=0;i<8;i++){//移位8次
SER=Byte&(0x80>>i);//因为第一个数据传输的是最高位数据(先传最后一个寄存器数据)
//所以用逻辑&来提取最高位数据
//SER只能一位一位传输,这样一次传输8位可以的原因是:如果要传输的数据
//为0,则SER送到寄存器里的为0,如果要传输的数据不为0,则SER
//送到寄存器里的为1
SRCLK=1;//将最高位下移一位
SRCLK=0;//清零
}
RCK=1;//将八位数据送到缓存寄存器
RCK=0;//清零
}
void MatrixLED_ShowColumn(unsigned char Column,Data)
{
_74HC595_WriteByte(Data);
P0=~(0x80>>Column);
Delay(1);
P0=0xFF;//清零显示,防止段选位选出现重影
}
void main()
{
SRCLK=0;//初始化SCK
RCK=0;//初始化
while(1)
{
MatrixLED_ShowColumn(0,0x3C);
MatrixLED_ShowColumn(1,0x42);
MatrixLED_ShowColumn(2,0xA9);
MatrixLED_ShowColumn(3,0x85);
MatrixLED_ShowColumn(4,0x85);
MatrixLED_ShowColumn(5,0xA9);
MatrixLED_ShowColumn(6,0x42);
MatrixLED_ShowColumn(7,0x3C);
}
}
5,应用2:显示动画
#include <REGX52.H>
#include "Delay.h"
#include "MatrixLED.h"
//用资料中的软件快速提取数据
//动画数据,动画数据如果很多的话,RAM内存可能会不够,而Flash的内存比RAM大得多,
//所以在此变量前加一个code,表示把它存在Flash中
//注意,放在Flash中以后此数组数据不可以再更改
unsigned char code Animation[]={
0x3C,0x42,0xA9,0x85,0x85,0xA9,0x42,0x3C,
0x3C,0x42,0xA1,0x85,0x85,0xA1,0x42,0x3C,
0x3C,0x42,0xA5,0x89,0x89,0xA5,0x42,0x3C,
};
void main()
{
unsigned char i,Offset=0,Count=0;
SRCLK=0;//初始化SCK
SRCLK=0;//初始化
while(1)
{
for(i=0;i<8;i++) //循环8次,显示8列数据
{
MatrixLED_ShowColumn(i,Animation[i+Offset]);
}
Count++; //计次延时
if(Count>15)
{
Count=0;
Offset+=8; //偏移+8,切换下一帧画面
if(Offset>16)
{
Offset=0;
}
}
}
}
十,DS1302实时时钟
普通单片机的定时器计时精度不高,而且会占用CPU的运行时间,除此之外,单片机时钟在断电后不会继续运行时钟会清零。
而DS1302时钟芯片带有备用电池,断电时会自动切换到备用电池,让他在单片机不上电时继续计时,而且在单片机有电时会对备用电池充电。DS1302是由美国DALLAS公司推出的具有涓细电流充电能力的低功耗实时时钟芯片。它可以对年、月、日、周、时、分、秒进行计时,且具有闰年补偿等多种功能
RTC:实时时钟,是一种集成电路,通常称为时钟芯片,DS1302,DS3231就是其中一种。
学一个芯片,最重要的就是会看手册
1,引脚定义和应用电路
DIP表示直插封装,SO表示贴片封装
共有8个引脚,第一部分表示电源VCC1.VCC2,GND,VCC2是主电源,VCC1是备用电池。X1,X2接晶振,晶振频率为32.768Hz,它通过内部电路处理会输出一个1Hz的标准频率,为实时时钟系统提供一个稳定的脉冲。然后单片机通过CE,IO,SCLK三个引脚将时钟芯片的信息读出来,或者把时间写进去。其数据输入输出方式和74HC595类似
引脚名 | 作用 | 引脚名 | 作用 |
VCC2 | 主电源 | CE | 芯片使能 |
VCC1 | 备用电池 | IO | 数据输入/输出 |
GND | 电源地 | SCLK | 串行时钟 |
X1、X2 | 32.768KHz晶振 |
2,内部电路结构
内部时间均存在寄存器RAM中。CE引脚作为芯片使能,相当于一个中介开关,输入移位寄存器的数据到命令控制逻辑后,首先要经过一个CE开关才能到实时时钟中,CE为高电平开关闭合,低电平断开,IO和SCLK就控制输入移位寄存器,IO相当于74HC595的SER一样用于输入数据
3,寄存器
- 与时钟有关的寄存器
共有81h~91h几个地址,每个地址有一个寄存器,一个寄存器有8位,其中如下图第一个地址中的寄存器表示秒。第二个地址中的寄存器表示分,以此类推。WP是write protect,是写保护,当它置为1时 ,写入的操作无效,置为0就可以写入,最后一个寄存器是用来存储涓流充电的
- 命令字
命令字用来控制是读还是写秒,是读秒还是读时等,即对上表选择的控制。
一个寄存器,总共八个字节,最高位固定为1不用管。如果要操作RAM,第6位就要给1,如果给0就是操作CK,即时钟。第5位到第1位就是我们要操作的地址。第0位是读写模式,如果给0就是写,给1就是读。这8位加起来就可以具体到时钟的地址。比如说要写入秒,那就给A4 A3 A2 A1 A0都为0,RD为0,那么这个寄存器就是1000 0000,换为16进制为0x80这就正好对应了秒的地址80h,如果要读秒,那就把RD置为1,则为1000 0001,换为16进制为0x81,正好对应了81h
4,时序图
根据时序图,时钟芯片工作时,先有单片机发一个命令字,再决定是读出还是写入,读出写入到哪里。完成后SCLK,CE均置零。
从CE线可以看出,要读写数据时CE要置为1,写完之后清零。SCLK就是给一个固定的时钟,IO就给数据,当SCLK为高电平,IO上的数据将会被写入,在时钟的下降沿,DS1302就会把他的数据输出,也就是说在时钟上升沿我用单片机给始终写入数据,在时钟下降沿,时钟芯片向单片机写入数据。(当RW给1时即为读数据,这时IO口由时钟芯片掌握,SCLK每输出一个下降沿数据读出一个。当RW给0时即为写数据,这时IO口由单片机掌握,SCLK每输出一个上升沿数据写入一个。)
5,代码实现过程
首先编写初始化函数,将CE和SCLK均置零
再编写写入函数(函数无返回值,参数为命令字和要输入数据共两个字节),参照下方写入的时序图,首先使CE=1。根据类似的74HC595的串行数据输入方式,这里的IO就是SER,给IO一个数据,然后置SCLK为1来存入寄存器。这里需要输入两个字节的数据,第一个字节的数据是命令字,取出命令字(主函数调用写入函数时给定)的第一个数据,置SCLK为1,再置为0,如此循环8次,完成命令字的输入,接下来数据的输入也类似,循环8次完成数据的写入。
再编写读取函数读取DS1302的数据(函数返回值为DS1302的数据,参数为命令字),命令字的输入过程同写入函数,有一个不同点,看下方读取时的时序图,可以看到读取的一个过程SCLK有15个脉冲,写入时SCLK有16个脉冲。这是因为读取时命令字的写入是来一个上升沿写入一次,而读取数据时IO口控制权交给DS1302,并且来一个下降沿读取一次,这就使其中的一个脉冲的上升沿和下降沿均有数据输入和读取,为了解决这个问题,在写入命令字时,在for循环内先令SCLK=1;在令其为0,这样在命令字写入结束时就正好在上升沿的位置,而不会经过下降沿。在读取数据的下降沿中,先令SCLK=1;再令其为0即可。
//DS1302.c
#include <REGX52.H>
sbit DS1302_SCLK=P3^6;
sbit DS1302_IO=P3^4;
sbit DS1302_CE=P3^5;
void DS1302_Init()
{
DS1302_CE=0;
DS1302_SCLK=0;
}
void DS1302_WriteByte(unsigned char Command,Data)
{
DS1302_CE=1;
int i=0;
//第一个for循环为命令字写入
for(i=0;i<=7;i++){
DS1302_IO=Command&(0x01<<i);//取出命令字的第0位
DS1302_SCLK=1;
DS1302_SCLK=0;//立马置为0要考虑时间问题,
//可以去数据手册看一下SCK这个传输过程所需时间为多少
//如果单片机处理速度快于SCK,则需要加延时函数
}
//第二个循环为数据写入
for(i=0;i<=7;i++){
DS1302_IO=Data&(0x01<<i);//取出命令字的第0位
DS1302_SCLK=1;
DS1302_SCLK=0;
}
DS1302_CE=0;//CE清零
}
unsigned char DS1302_ReadByte(unsigned char Command)
{
DS1302_CE=1;
int i=0;
unsigned char Data=0x00;
for(i=0;i<=7;i++){
DS1302_IO=Command&(0x01<<i);//取出命令字的第0位
DS1302_SCLK=0;
DS1302_SCLK=1;//使时序相同而调换的顺序
}
for(i=0;i<8;i++){
DS1302_SCLK=1;
DS1302_SCLK=0;
if(DS1302_IO){Data|=(0x01<<i);}
}
DS1302_CE=0;
return Data;
}
- 常见问题:
1,如果在主函数执行一个简单例子,如下代码显示是可以正常显示的,但如果把Num和LCD_ShowNum放到while中,LCD屏就会出现数字显示错误并且不清的问题 。改进方法是对 unsigned char DS1302_ReadByte(unsigned char Command)函数做如下处理
unsigned char DS1302_ReadByte(unsigned char Command)
{
DS1302_CE=1;
int i=0;
unsigned char Data=0x00;
for(i=0;i<=7;i++){
DS1302_IO=Command&(0x01<<i);//取出命令字的第0位
DS1302_SCLK=0;
DS1302_SCLK=1;//使时序相同而调换的顺序
}
for(i=0;i<8;i++){
DS1302_SCLK=1;
DS1302_SCLK=0;
if(DS1302_IO){Data|=(0x01<<i);}
}
DS1302_CE=0;DS1302_IO=0;
return Data;}
#include <REGX52.H>
#include "Delay.h"
#include "DS1302.h"
#include "LCD1602.h"
unsigned char Num=0x00;
void main()
{
DS1302_Init();
LCD_Init();
LCD_ShowString(1,1,"RTC");
DS1302_WriteByte(0x80,0x30);
Num=DS1302_ReadByte(0x81);
LCD_ShowNum(1,1,Num,3);
while(1)
{
//Num=DS1302_ReadByte(0x81);
//LCD_ShowNum(1,1,Num,3);
}
}
2,如果读出时间为一个大于59并且不动的数,则芯片可能处于 写保护状态,在DS1302_WriteByte之前加上DS1302_Write(0x8E,0x00)即可解除芯片写保护
3,解决完第1个问题后,LCD显示正常,但在计数时,9会突然变到16。这是因为时钟芯片内的寄存器不是按照正常二进制进行存储的,而是以BCD码进行存储的(BCD码是用4位二进制码来表示1位十进制数,如0001 0011表示13,1000 0101表示85,1010表示10不合法;在十六进制中的体现为0x13表示13,0x85表示85,0x1A不合法)。在BCD编码中0000 1001表示9时,下一位0001 0000表示10,而0001 0000在二进制中表示16,所以会产生突变。可以看到BCD码在16进制中的显示是可以代表十进制数的,因为BCD码的0001 0011就可以转换为0x13,13就是要表示的数。所以第一种解决办法是以16进制显示BCD编码的数。第二种方法可以将BCD转换为十进制,它的公式如下:所以就可以写成LCD_ShowNum(1,1,Num/16*10+Num%16,3);
上述的BCD编码形式在我们之前看的芯片寄存器上也有体现:以秒为例,前4位为Seconds,后三位为10Seconds,也就是说后三位表示十位数字,CH表示时钟静止,如果置1则秒停止计数。
6,应用1:时钟显示
年月日数据很多,都在主函数里调用很麻烦。接下来对DS1302.c进行改进
//DS1302.c
#include <REGX52.H>
sbit DS1302_SCLK=P3^6;
sbit DS1302_IO=P3^4;
sbit DS1302_CE=P3^5;
#define DS1302_SECOND 0X80;
#define DS1302_MINUTE 0X82;
#define DS1302_HOUR 0X84;
#define DS1302_DATE 0X86;
#define DS1302_MONTH 0X88;
#define DS1302_DAY 0X8A;
#define DS1302_YEAR 0X8C;
#define DS1302_WP 0X8E;
unsigned char DS1302_Time[]={24,6,21,16,24,55,6}//定义一个数组存放时间
//24年6月21日16时24分55秒,星期六
void DS1302_Init()
{
DS1302_CE=0;
DS1302_SCLK=0;
}
void DS1302_WriteByte(unsigned char Command,Data)
{
DS1302_CE=1;
int i=0;
//第一个for循环为命令字写入
for(i=0;i<=7;i++){
DS1302_IO=Command&(0x01<<i);//取出命令字的第0位
DS1302_SCLK=1;
DS1302_SCLK=0;//立马置为0要考虑时间问题,
//可以去数据手册看一下SCK这个传输过程所需时间为多少
//如果单片机处理速度快于SCK,则需要加延时函数
}
//第二个循环为数据写入
for(i=0;i<=7;i++){
DS1302_IO=Data&(0x01<<i);//取出命令字的第0位
DS1302_SCLK=1;
DS1302_SCLK=0;
}
DS1302_CE=0;//CE清零
}
unsigned char DS1302_ReadByte(unsigned char Command)
{
DS1302_CE=1;
int i=0;
unsigned char Data=0x00;
for(i=0;i<=7;i++){
DS1302_IO=Command&(0x01<<i);//取出命令字的第0位
DS1302_SCLK=0;
DS1302_SCLK=1;//使时序相同而调换的顺序
}
for(i=0;i<8;i++){
DS1302_SCLK=1;
DS1302_SCLK=0;
if(DS1302_IO){Data|=(0x01<<i);}
}
DS1302_CE=0;
return Data;
}
void DS1302_SetTime()//调用此函数,将上述定义的时间数组设置到时钟芯片中去
{
DS1302_WriteByte(0x8E,0x00);//关闭写保护
DS1302_WriteByte(DS1302_YEAR,DS1302_Time[0]/10*16+DS1302_Time[0]%10);
//将其转换为BCD码放入
DS1302_WriteByte(DS1302_YEAR,DS1302_Time[0]/10*16+DS1302_Time[0]%10);
DS1302_WriteByte(DS1302_MONTH,DS1302_Time[1]/10*16+DS1302_Time[0]%10);
DS1302_WriteByte(DS1302_DATE,DS1302_Time[2]/10*16+DS1302_Time[0]%10);
DS1302_WriteByte(DS1302_HOUR,DS1302_Time[3]/10*16+DS1302_Time[0]%10);
DS1302_WriteByte(DS1302_MINUTE,DS1302_Time[4]/10*16+DS1302_Time[0]%10);
DS1302_WriteByte(DS1302_SECOND,DS1302_Time[5]/10*16+DS1302_Time[0]%10);
DS1302_WriteByte(DS1302_DAY,DS1302_Time[6]/10*16+DS1302_Time[0]%10);
}
void DS1302_ReadTime()//调取此函数,将时钟芯片内的数据读取回来存到时间数组中
{
unsigned char Temp;
Temp=DS1302_Time[0]=DS1302_ReadByte(0x8D);
DS1302_Time[0]=Temp/16*10+Temp&16;
Temp=DS1302_Time[1]=DS1302_ReadByte(0x89);
DS1302_Time[1]=Temp/16*10+Temp&16;
Temp=DS1302_Time[2]=DS1302_ReadByte(0x87);
DS1302_Time[2]=Temp/16*10+Temp&16;
Temp=DS1302_Time[3]=DS1302_ReadByte(0x85);
DS1302_Time[3]=Temp/16*10+Temp&16;
Temp=DS1302_Time[4]=DS1302_ReadByte(0x83);
DS1302_Time[4]=Temp/16*10+Temp&16;
Temp=DS1302_Time[5]=DS1302_ReadByte(0x81);
DS1302_Time[5]=Temp/16*10+Temp&16;
Temp=DS1302_Time[6]=DS1302_ReadByte(0x8B);
DS1302_Time[6]=Temp/16*10+Temp&16;
}
DS1302.h文件
#ifndef __DS1302_H__
#define __DS1302_H__
void DS1302_Init();
void DS1302_WriteByte(unsigned char Command,Data);
unsigned char DS1302_ReadByte(unsigned char Command);
void DS1302_ReadTime();
void DS1302_Settime();
#endif
main.c文件
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
extern unsigned char DS1302_Time[];
void main()
{
LCD_Init();
DS1302_Init();
LCD_ShowString(1,1," - - ");//静态字符初始化显示
LCD_ShowString(2,1," : : ");
DS1302_SetTime();//设置时间
while(1)
{
DS1302_ReadTime();//读取时间
LCD_ShowNum(1,1,DS1302_Time[0],2);//显示年
LCD_ShowNum(1,4,DS1302_Time[1],2);//显示月
LCD_ShowNum(1,7,DS1302_Time[2],2);//显示日
LCD_ShowNum(2,1,DS1302_Time[3],2);//显示时
LCD_ShowNum(2,4,DS1302_Time[4],2);//显示分
LCD_ShowNum(2,7,DS1302_Time[5],2);//显示秒
}
}
7,应用2:可调时钟
十一,蜂鸣器
1,蜂鸣器介绍



2,驱动电路
(1)三极管驱动
三极管驱动电路中的三极管作为开关
左边电路图中R1左端接单片机IO口,如果IO口给高电压则导通,蜂鸣器响,如果给低电平,三极管电路截至。所以单片机在控制蜂鸣器中起信号控制作用。R1是限流电阻,保证三极管能导通就以
(2)集成电路驱动
由于单片机不能直接驱动元器件(输出高电平不稳定),所以就采用一个芯片来驱动,有点类似于之前说过的双向数据缓冲器74HC245
ULN2003驱动芯片
达林顿管是一种三极管复合的晶体管,可以增强放大能力
如下,ULN2003由7对非门组成,每个NPN达林管组成每个非门,当左端接单片机后,单片机给1,则右边输出为0,以此来驱动元器件。(注意:如果左端给0,右端输出的1不具有驱动能力,实际上相当于右端断路,处于高阻态状态。)
根据上述的电路原理图,给P15一个高电平就可以控制蜂鸣器了
十二,AT24C02(I2C总线)
1,存储器内部简化模型
存储器内部实际上是电路的网状结构,如下图,横向线称为地址总线(用来选中所需的线),竖向线称为数据总线。存储原理:比如在地址总线选中了第一行,可以给他加一个高电平1,其他地址总线都不接。然后把左上角第一个节点两条线连上,然后从左到右连接第二个节点,第三个节点,其他节点不连接(各线的相交处是不连接的,在要存储时才会连接)。前三个节点连上以后,数据总线的前三根线就会变为1。所以在第一根线也就是第一个地址下就存了1110 0000,以此来存储数据。实际上两条线交错处的节点不是简单的相连,而是通过二极管相连,防止每个节点之间的互相干扰。在最初的Mask ROM中,如果这个节点什么都不接如左图,即相当于无数据,当需要将交错线接到一起时,就连接一个二极管,是固定的电路。而PROM中对头连接两个二极管,只需要击穿其中一个二极管就可以通电,所以PROM可以通过编程给二极管两端击穿电压,达到编程存储的效果,由于二极管击穿后就永久损坏了,所以PROM只能编程一次。
由于地址总线一次只能选中一行,所以常在地址总线之前加一个138译码器
2,AT24C02介绍
(1)简介



(2)引脚及应用电路
AT24C02共有8个引脚,WP是写保护,高电平时处于写保护状态。
根据电路图,使用时给AT24C02接上电源,A0~A2接到VCC或者GND,SCL和SDA接外界电路,下面介绍I2C时会详细介绍
3,I2C总线介绍
(1)简介


(2)I2C电路规范
弱上拉模式 :前面介绍过,电路图如下
开漏输出模式:开漏输出模式没有上拉电阻,当开关闭合时,I/O输出为低电平0,开关断开时(信号1),引脚处于浮空状态,此时相当于电路断路,一点小的干扰就会使引脚电压变化,电压不稳定
(3)I2C时序结构
起始条件(即要开始通信之前给各设备一个要开始的信号):SCL高电平期间,SDA从高电平切换到低电平。把SCL最后拉低是为了和下一个时序衔接起来,保证下一个时序不用从高再到低。
终止条件:
发送一个字节:发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位在前),然后拉高SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。如下图,SDA画叉的地方是数据变化的时候,数据传输时每次传输一位,画叉的地方只是为了便于理解画了两条线,实际上只有一条,当要传输的第一位数据为1时,则线从低电平升高到高电平或者从高电平继续保持高电平
所有线都是由主机控制,这也是由主机发送的,从机(AT24C02)会读取SCL,SDA数据,会自动读取。
接受一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位在前),然后拉高SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA,释放后主机将控制权交给从机),此时从机控制主线,主机接收数据
发送应答:在接收完一个字节之后,主机在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
接收应答:在发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)
可以作为数据发送的第九位
(4)I2C数据帧
发送一帧数据:第一个是发送开始标志S,在之后第一节数据一定要发送从机地址+读写位(共八位,前七位为地址位,其中前四位为固定端,AT24C02固定为1010,其他芯片不一样,后三位可配置,对应AT24C03的A0,A1,A2引脚),然后每发一个字节都跟一个RA接收应答,之后再发送数据字节,发送完最后一节后,应答完跟一个终止。
接收一帧数据 :
先发送再接收数据帧 :
(5)AT24C02数据帧
但是AT24C02实际应用起来并不会像上述写入一帧数据一样byte1,byte2,byte3一直发 ,而是用如下数据帧写入,它的数据帧和上述的I2C数据帧类似,但有所不同。
字节写: 继承了发送一帧数据。字节写只写入一个字节,页写和I2C写入一帧数据一样可以写入多节数据,这里不做介绍。
随机读: 继承了复合格式
此外,还有多种数据帧形式,详细可以见手册。
十三,DS18B20温度传感器
1,基本介绍
DS18B20是一种常见的数字温度传感器,其控制命令和数据都是以数字信号的方式输入输出,相比较于模拟温度传感器(热敏电阻等),具有功能强大,硬件简单,易扩展,抗干扰性强等特点。测温范围在-55度到125度。通信接口为1-Wire(单总线)。可形成总线结构,内置温度报警功能,可寄生供电(DS10B20的三个引脚有两个是供电的,剩下一个用来数据输入输出。如果使用寄生供电,可以直接连接一个GND和数据线就可以实现功能)。
2,引脚及其应用电路
DQ线上的上拉电阻R1是为了实现总线操作,与I2C中的上拉电阻作用一样。
3,内部电路结构
DS18B20内部是有一个模拟温度传感器的,通过内部的一些控制器把数据(电压变化)都出来放在RAM中,我们再通过通信协议将RAM中的数据读出来
如下图所示,第一个矩形框所示为寄生电源电路,可以省去VDD,省去VDD总要有电源正极,而这个电源正极就是从DQ进来的;正常情况下有VDD内部电路走向如红线所示进入INTERNAL VDD(内部VDD);如果VDD没有接(将VDD直接接到GND即可),那红线所示就不会有电流,这时电路就会从DQ取出电源正极,电流流向如蓝线所示进入INTERNAL VDD(内部VDD),同时,绿线电流流入Cpp,这里的Cpp电容就相当于是电源,当DQ给高电平时就对电容充电,当DQ为低电平时电容就作为电源给INTERNAL VDD(内部VDD)供电。如果采用寄生供电时,内部出现一些比较耗电的操作时,我们还要给DQ一个强上拉(即给一个很高的电源正极,以达到内部供电的稳定)。紫框所示的POWER SUPPLY SENSE是一个电源供给的感应,可以感应外部VDD是否存在,如果不存在会调节内部的结构来省电。寄生电源电路是自动运行,不需要软件配置
如下图红线所示,DQ的数据首先要经过64 BIT ROM(64位只读存储器,作为器件地址,用于总线通讯的寻址)和1-Wire PORT(单总线接口) ,64 BIT ROM AND 1-Wire PORT这个框就相当于整个DS18B20通信的一个大门,外部发送正确的地址才能通过这个大门(也可以选择指令跳过这个大门)。继续往下走就会经过MEMORY CONTROL LOGIC这个框,是指内存的一个控制逻辑,可以理解为DS18B20这个房子的管家,如果通过大门就会与他交互,他直接掌管着内部的一个RAM(SCRATCHIPAD,中文意思为暂存器,就是所说的RAM,它用于总线的数据交互),当数据通过大门时就会与管家进行交流,如果是读,管家就会把RAM的数据放到总线上,如果是写管家就会把数据写入RAM中,RAM只是纯数据的一个盒子,他与右边紫框所示的设备进行关联,其中第一个就是TEMPERTUREE SENSOR,即温度传感器就相当于内部的模拟温度传感器,当我们发出指令让它开始温度转换的时候,这个温度传感器就工作,然后把数据放在RAM里面。
TEMPERTUREE SENSOR下面的一个设备ALARM HIGH TRIGGER(报警高出发寄存器),用来存储温度上限阈值,用于温度报警的。CONFIGURATION REGISTER是配置寄存器,里面就存了一个东西,就是可以设置精度。最后一个8-BIT CRC GENERATOR(8位CRC生成器),CRC就是一种校验码,它会通过一种特殊的格式,把RAM之前的一些数据进行一个校验,得到一个校验码放在后面,就是用于判断数据是否正确的,如果输入数据任何一位出错都得不到校验码所示的数字,CRC是一种校验正确率很高的效验码算法。
4,RAM存储器结构
如下表也就对应了上标RAM,其中右边EEPROM中的三个对应了上图紫框中间的三个。RAM共9位,前两位存放的是温度,两位共同组成温度的一个数据,括号里的85是一个默认值,就上电直接读会显示85℃。
5,单总线介绍
单总线(1-Wire BUS)是由Dallas公司开发的一种通用数据总线,使用一根数据线DQ,通信方式为异步半双工。单总线只需要一根通信线即可实现数据的双向传输,当采用寄生供电时,还可以省去设备的VDD线路,此时,供电加通信只需要DQ和GND两根线
6,单总线电路规范
硬件配置要求:设备的DQ均要配置成开漏输出模式;DQ要添加一个上拉电阻,阻值一般为4.7kΩ左右,上述两条要求和I2C相同;若此总线的从机采用寄生供电,则主机还应配一个强上拉输出电路
下图所示为DS18B20采用寄生电路时的电路配置。其中红框内所示为MOS管,当μp(主机)I/O口输出低电平时MOS管打开,DQ接到电源Vpu。
7,单总线时序结构
初始化
初始化(开启):主机将总线拉低至少480us,然后释放总线,等待15~60us后,存在的从机会拉低总线60~240us以响应主机,之后从机将释放总线。
对照下图,当处于空闲状态时Vpu也就是总线处于高电平,当要开始通信时,主机将总线改为低电平,且这个低电平最少持续480us。然后主机释放总线(不是拉高电平!),释放总线后,电平由上拉电阻拉高,可以看到图中电平被拉高的过程是弯的,这是为了表达弱上拉电阻(不会把电平立即拉高)的意思。等待15~60us后,如果从机存在,则会拉低总线电平60~240us来响应主机。最后从机释放,总线被上拉电阻重新拉回到高电平,进入空闲状态
发送一位
发送一位:主机将总线拉低60~120us,然后释放总线,表示发送0;主机将总线拉低1~15us,然后释放总线,表示发送1。从机将在总线拉低30us后(典型值)读取电平,整个时间片应大于60us
接收一位
接收一位:主机将总线拉低1~15us,然后释放总线,并在拉低后15us内读取总线电平(尽量贴近15us的末尾),读取为低电平则为接收0,读取为高电平则为接收1 ,整个时间片应大于60us
8,DS18B20操作流程
- 初始化:从机复位,主机判断从机是否响应
- ROM操作:ROM指令+本指令需要的读写操作 (进入大门的操作)
- 功能操作:功能指令+本指令需要的读写操作(操作寄存器RAM)
9,DS18B20数据帧
利用上述指令构成数据帧
数据帧一般组成形式:初始化->ROM指令->功能指令
当调用CONVERT功能指令时从机就会立马把温度传感器的值给刷新到RAM暂存器里面去
温度读取过程中也是差不多的,先调用初始化,再调用ROM指令,然后调用功能指令,由于调用的是读暂存器指令,所以后面要跟读的内容,下图选择读的是Byte0和Byte1,除此之外也可以再读Byte2等等。
以下为字节Temperture LSB和 Temperture MSB存储数据的形式,其中BIT15到BIT11这5位是符号位,如果是负数则全为1,如果是正数则全为0.
下图为举例。其中负数是补码形式,将所有位取反然后加1才能得到正数值
十四,直流电机驱动(PWM)
1,基本介绍
直流电机是一种将电能转换为机械能的装置。一般的直流电机有两个电极,当电极正接时,电机正转,当电极反接时,电机反转.直流电机主要由永磁体(定子)、线圈(转子)和换向器组成。除直流电机外,常见的电机还有步进电机、舵机、无刷电机、空心杯电机等 。
2,电机驱动电路
电机属于功率比较大的负载,如果直接接在单片机IO口上是驱动不了的,而且还可能损坏单片机IO口,所以需要在单片机和IO口之间加一个驱动电路。常用驱动方式有大功率器件直接驱动和H桥驱动。
大功率器件直接驱动
这种驱动方式只能驱动电机朝一个方向转动,不具备调换电机正反方向的功能
基本结构相当于三极管开关,这个三极管要求我们选择一个功率比较大的器件,比如达林顿三极管,MOS管等等。单片机IO口IN输出低电平三极管导通,二极管D1用来保护电路,称为续流二极管。正常情况下,二极管D1不导通,电流经过电机B1流向地;因为电机是一种感性负载元件,驱动时要注意感性值(电感特性),即会感应出一个很高的电压(可能会超过电源电压),如果IN输出为1时,电路截至,由于电机电感的存在会感应出高的电压,可能会击穿三极管甚至是IO口,而加了续流二极管后,电机形成的感应电流就在二极管和电机组成的回路中释放掉。
H桥驱动
这种电路可以驱动电机正反转
如果我们使晶体管Q1和Q4导通,Q2和Q3断开电机就朝一个方向转动;如果使Q2和Q3导通,Q1和Q4断开电机就朝相反方向转动。由于没有续流二极管,所以选择的三极管或者MOS管要有很强的耐压特性
实际应用中使用电机驱动芯片即可。
3,PWM驱动直流电机
利用定时器实现PWM。
#include<REGX52.H>
sbit Motor=P1^0;
unsigned char Counter,Compare; //计数值和比较值,用于输出PWM
//100us中断一次的定时函数
void Timer0_Init(void)
{
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x9C; //设置定时初值
TH0 = 0xFF; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0=1;
EA=1;
PT0=0;
}
void main()
{
Timer0_Init();
while(1)
{
}
}
void Timer0_Routine() interrupt 1
{
TL0 = 0x9C; //设置定时初值
TH0 = 0xFF; //设置定时初值
Counter++;
Counter%=100; //计数值变化范围限制在0~99
if(Counter<Compare) //计数值小于比较值
{
Motor=1; //输出1
}
else //计数值大于比较值
{
Motor=0; //输出0
}
}
十五,AD/DA
1,AD/DA基本介绍
AD(Analog to Digital):模拟-数字转换,将模拟信号转换为计算机可操作的数字信号 。DA(Digital to Analog):数字-模拟转换,将计算机输出的数字信号转换为模拟信号。AD/DA转换打开了计算机与模拟信号的大门,极大的提高了计算机系统的应用范围,也为模拟信号数字化处理提供了可能。
2,硬件电路模型
- AD转换通常有多个输入通道,用多路选择开关连接至AD转换器,以实现AD多路复用的目的,提高硬件利用率。而DA一般只有一个通道输出。
- AD/DA与单片机数据传送可使用并口(速度快、原理简单),也可使用串口(接线少、使用方便)
- 可将AD/DA模块直接集成在单片机内,这样直接写入/读出寄存器就可进行AD/DA转换,单片机的IO口可直接复用为AD/DA的通道
- DA用的比较少,它在一定程度上可以用PWM代替
实用芯片介绍:着重介绍两款比较老的AD/DA芯片 ADC0809和DAC0832
ADC0809的模拟信号是从IN接口(共8个)来从OUT得到数字量输出。输入的模拟信号首先进入8位模拟开关(共有8路通道可以进入到A/D转换),下方的地址所存与译码是通过ADDA ADDB ADDC ALE引脚来控制的,控制的结果就是可以选择输入的模拟信号从8路模拟开关进入A/D转换通过的通道。一个芯片内只有一个A/D转换,但它可以同时读入8路。A/D转换上方的START EOC CLOCK可以控制A/D转换的启动(START),结束(EOC),时钟信号(CLOCK);通过这些就可以把输入的电压信号转换为数字信号,然后通过一个锁存器进行输出缓存,最后通过缓存器上的OE输出使能就可以把8位数据给输出出去。等会分析电路主要分析A/D转换电路
相对于ADC来说,DAC构造原理会相对简单一些。DAC0832由输入的D0~D7八位数据,经过8位输入寄存器(缓存器)和8位DAC寄存器(缓存器)(由下方的ILE,CS,WR1等等控制器控制);上述的两个寄存器主要用来多路同步,比如说有两个DAC,要求这两个DAC同步输出,可以先把这两个DAC分别放到两个缓存器里,等两个缓存器数据放好了后再个输出信号,这样两个DAC就可以同步输出。之后再经过8位D/A转换器就可以输出模拟信号
3, AD/DA原理
运算放大器

如下,为LM358电路,用上运算放大器的内部模块
四种常见的运放电路
电压比较器
它会比较同相输入端和反相输入端两个信号的电压值,然后以高低电平的形式输出。它是开环状态(即无负反馈)。它的输出形式如下
反向放大器
可调放大倍数的放大器。放大调节如下图所示。反相放大器需要接双电源(比如+12V和-12V)才能输出大于输入电压的负电压。
同向放大器
电压跟随器
输出电压等于输入电压,在电路中使用电压跟随器可以提高信号驱动能力,因为运算放大器内部是有功率放大器的,当放大倍数为1时,虽然没有电压放大特性,但是有功率放大特性。
DA原理
如下为T型电阻网络DA转换器,它就是我们在上图DAC0832中所说的8位D/A转换器。
电路下方D0~D7是输入的8位数字量(0或1),控制的是模拟开关接0或者是1,最终输出是运算放大器的输出,运算效果如下公式。VREF指的是参考电压,假如是5V的话,电路就会对这5V电压进行256等分,通过D0~D7组成的二进制数决定取其中的多少份。
原理过程:由运算放大器同相和反相输入端虚短可知,无论开关是接到0或者1,都相当于接地。由虚断可知,从上方各电阻流过的电流将直接流向Rfb而不留向放大器反相输入端。电路图中的电阻只有R和2R两种,最右边的两个2R并联起来相当于一个总阻值为R的电阻,而这两个2R又和干路上的R串联组成电阻为2R的电阻,而这3个电阻组成的2R又和再上一个2R电阻并联又形成2R的电阻,所以除了第一个电阻(流过电流I7的电阻)其他电阻加起来还是2R,而第一个电阻和其他所有电阻再并联,也就是所有电阻加起来电阻就是R,所以总电流I就等于VREF/R,支路电流关系就是I1=2I0,I2=2I1.。。。。以此类推,左边的电流都是右边电流的两倍,这就实现了二进制的每一位,I7=2I6=4I5=。。。。=2的8次方倍的I0。
因为是反向放大器,所以输出电压为负数
PWM型DA转换器
将PWM信号加一个低通滤波器(RC滤波),如图加了两个低通滤波器,叫做二节低通滤波器,滤波效果更好。PWM信号有一个直流分量和一个交流分量,低通滤波器截止频率低,可以把交流分量给滤掉,剩下的就是直流分量(PWM信号的平均值),由于低通滤波器分压结构得到的信号驱动能力很弱如果接负载会影响滤波性能,所以接一个电压跟随器,增大驱动能力,同时隔离电路。
AD原理
以下为AD经常使用的逐次逼近型AD转换器。DA可以用数字量控制开关,开关量输出模拟信号。
当电压信号通过通道选择开关进来之后,通过一个比较器,DAC输出一个电压信号与其进行比较(比如说参考电压VREF为5V,那就先输出2.5V进行比较如果比进来的电压信号大,那就再用1.25V进行比较,这样采用二分法逐渐逼近,最终达到与输入电压接近的数值,对于8位的DAC,最多只需要8次就可以找出接近值),如果比进来的信号电压大,则DAC降低电压如果小则升高,最终让这两个电压接近相等,那DAC中的已知数字量就可以表示进来的未知数字量,最终用8位三态锁存缓冲器将DAC中的数字量输出
AD/DA性能指标
•分辨率:指AD/DA数字量的精细程度,通常用位数表示。例如,对于5V电源系统来说,8位的AD可将5V等分为256份,即数字量变化最小一个单位时,模拟量变化5V/256=0.01953125V,所以,8位AD的电压分辨率为0.01953125V,AD/DA的位数越高,分辨率就越高
转换速度:表示AD/DA的最大采样/建立频率,通常用转换频率或者转换时间来表示,对于采样/输出高速信号,应注意AD/DA的转换速度
4,XPT2046
它的时序是对SPI的通信。SPI通信的基本信号线:CS片选,DCLK时钟线,DIN输入数据,DOUT 。它是可以复用的,可以一组总线挂多个负载。DCLK,DIN,DOUT是共用的,而每个芯片有一个单独的CS片选,在同一时间我只选中一片,共用的三个线和这一片进行通信,从而不冲突。DCLK控制输入输出(上升沿输入,下降沿输入),DIN和DOUT可以同时输入和输出。开始通信时CS拉低,DCLK上升沿则发送一个字节,前8位为控制位,由DIN输入,它用来启动转换,寻址,设置ADC分辨率,配置和对XPT2046进行掉电控制。第一位S是起始位,控制字首位必须为S,且S必须为1,在XPT2046的DIN引脚检测到起始位前,所有的输入都将被忽略。A2 A1 A0 是地址,用来选择多路选择器的现行通道,触摸屏驱动和参考源输入。MODE是模式选择位,用于设置ADC的分辨率,MODE=0表示12位模式,MODE=1表示8位模式。SER/DFR位控制参考源模式,SER/DFR=1表示单端模式(只读X+接口来的模拟信号,X-口接地即可),SER/DFR=0表示差分模式(读X-和X+来的模拟信号,并放大他们的差分信号)。PD0和PD1表示低功率模式选择位,若为11则器件总处于供电状态,若为00则器件在变换之间处于低功率模式;PD1还可以选择内部参考电压,如果给1它就选择内部的参考电压(2.5V),给他0就用外部参考电压(自己接的电压VREF)。然后发送完控制位后可以连续读取两个字节
地址A0 A1 A2配置如下如果要测X+(即XP),则A0 A1 A2为100或者011,控制字给1001 1100(十六进制为0x9C);要测Y+,A0 A1 A2设为101,控制字为0xDC,要测VBAT,A0 A1 A2设为010,控制字为0xAC
#include <REGX52.H>
#include <INTRINS.H>
//引脚定义
sbit XPY2046_DIN=P3^4;
sbit XPY2046_CS=P3^5;
sbit XPY2046_DCLK=P3^6;
sbit XPY2046_DOUT=P3^7;
/**
* @brief ZPT2046读取AD值
* @param Command 命令字,范围:头文件内定义的宏,结尾的数字表示转换的位数
* @retval AD转换后的数字量,范围:8位为0~255,12位为0~4095
*/
unsigned int XPT2046_ReadAD(unsigned char Command)
{
unsigned char i;
unsigned int Data=0;//16位数据
XPY2046_DCLK=0;
XPY2046_CS=0;//初始状态设置
for(i=0;i<8;i++)
{
XPY2046_DIN=Command&(0x80>>i);//一位一位给芯片输入控制字
XPY2046_DCLK=1;
XPY2046_DCLK=0;
}
for(i=0;i<16;i++)
{
XPY2046_DCLK=1;
XPY2046_DCLK=0;
if(XPY2046_DOUT){Data|=(0x8000>>i);}//一次读出16位数据
}
XPY2046_CS=1;
return Data>>8;//如果为8位数据分辨率模式,则后八位会补零使数据增大,
//为了不影响数据大小,右移8位
//如果为12位分辨率模式则右移4位
}
十六,红外遥控(外部中断)
1,简介
2,硬件电路
发送部分
如下第一个图,由两个三级管开关和一个LED红外光二极管组成。这两个三极管同时打开时红外灯才能亮,第一个三极管基极为38kHZ的调制频率,一直输入38KHZ的方波即可,IN口就是我们想输入的波形。三极管是PNP的,低电平导通,所以IN要是低电平下面才能导通;如果IN是低电平那LED亮不亮就取决于38KHZ是高电平还是低电平;他们两个加起来的效果就是如果IN为高电平它不亮,如果是低电平则以38KHZ闪着亮。这样做的目的是为了抗干扰,自然界中有很多红外光,如果LED1以连续的方式亮就会受到干扰,但是如果以38KHZ频率亮,接收头就可以提取出LED1的灯光。第二个电路虽然硬件看起来简单,但是也是实现第一个电路的功能,只不过要通过软件去实现。
接收部分
普通的红外接收管会把自然界中所有红外光都接收,然后通过后续电路处理过滤掉自然光等的干扰,然后再把38KHZ的红外光滤掉,只留下IN口输入的方波,但是这个过程需要很大一部分电路,为了方便,我们可以使用一体化红外接收头,它里面包含了红外接收管和滤波电路,然后直接输出我们需要的信号(OUT输出)。由于发射一次红外光会产生多个高低电平,而且这些高低电平会在很短的时间内结束,所以不能像单片机判断按键一样if while,为了更快的处理需要把OUT引脚接到外部中断引脚上,如果他一旦产生下降沿就立马中断进行处理,这样的话响应实时性会很高
发送与接收过程如下
3,NEC编码
以下波形即为遥控器按下之后,接收头OUT 引脚输出的波形。首先没有按键按下,这个输出波形就是高电平,一旦有按键按下,OUT就会输出一个start信号(信号组成如下红色波形)(告诉单片机按键开始了,要开始发送数据了),start信号结束后就有一长段的数据区,表示遥控器的地址码和控制码,DATA格式如下一共4个字节(32bit),第一个字节表示地址码也就是遥控器的标识符,然后跟一个地址码反码用于数据验证,然后是命令码,就是键码,代表我们按下了哪个按键,最后接命令码反码进行数据验证。(每个字节都是低位在前高位在后)。逻辑0和1规则如下(与高低电平不对应),以110ms为周期为一帧数据。repeat就是按下按键不放手,则会每隔100ms发送一个repeat