一、定时器
1.定时器介绍
2.STC89C52定时器资源
3.定时器框图
4.定时器工作模式
定时器分为三部分:时钟、计数系统、中断
5.定时器时钟
单片机的时钟可以由系统时钟来提供,也可以由外部引脚来提供,当由外部引脚来提供时钟的时候,定时器就是一个计数器,外部引脚每来一个脉冲就会加一,相当于计脉冲的一个计数器。
该开发板上使用的为12MHz的晶振,即系统时钟为12MHz,将12MHz的脉冲进行12分频(此处默认为12T模式),分频之后变为1MHz,1个周期=1us,此时计数单元每隔1us就要计数一次,当其计到最大值时会产生中断。
开关处,Counter计数器,Timer计时器,C/T=0时是定时方式,C/T=1是计数方式。
6.中断系统
中断系统主要是用来提醒主程序。高优先级的中断可以打断低优先级的中断。
7.中断程序流程
8.STC89C52中断资源
传统的8051单片机只有5个中断源(外部中断0、定时器0中断、外部中断1、定时器1中断、串口中断)、2个中断优先级。
9.定时器和中断系统
单独的定时器也可以工作,只不过没有中断主程序就不能响应了。
10.定时器相关寄存器
单片机通过配置寄存器来控制内部线路的连接,通过内部线路的不同连接方式来实现不同电路完成不同功能。
GATE是门控端,不常用。TF一般只读不写。
11.代码示例
不可位寻址的寄存器只能整体赋值,可位寻址的寄存器可以对其每一位单独赋值。
TF是中断溢出标志位,将其置零是为了防止其刚配置好就产生中断。
TR负责控制定时器是否开启。
IE和IT是控制外部中断引脚,GATE=0时,这一部分可以不用配置。
PT0通常默认为0
上电时需要调用该函数进行初始化。
上述方式进行TMOD赋值时,若只使用一个定时器则无影响,使用两个定时器时会产生冲突。所以采用“与或式赋值法”。
TH0和TL0计算的值比生成的值小1us。
注意需要在中断函数中赋初值。
static是静态变量,如果不加 static 则局部变量在函数结束之后会销毁,下次再调用局部变量时就不是上次的局部变量了,为了保证函数结束后静态变量的值仍保留,需要设置为静态变量。
定时器一般不容易模块化,常写在主函数中。
这个头文件中包含许多函数,此处要用到的是循环左移和循环右移
(1)按键控制LED流水灯模式
#include <REGX52.H>
#include "Timer0.h"
#include "Key.h"
#include <INTRINS.H>
unsigned char KeyNum,LEDMode;
void main()
{
P2=0xFE;
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);
}
}
(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
}
}
}
}
}
二、串口通信
1.串口介绍
2.硬件电路
TXD:Tansmit Exchange Data 发送端
RXD:Receive Exchange Data 接收端
VCC可以不接,GND一定要连接。当二者都有独立电源进行供电时,VCC可以不接。
3.电平标准
TTL:Transistor Transistor Logic
单片机中使用的就是TTL电平。
TTL电平和RS232电平的数据传输距离有限,仅十几米时3错误率就会很高,RS485电平数据传输的距离在一千米以上。
RS232电平一般用于电脑等高电压之间的传输,所容忍的电压变化范围比较大,稳定性比TTL电平更好。但是在弟弟淡雅单片机系统中使用TTL电平足矣。
4.接口及引脚定义
VGA接口与串口外观类似,但是它有3排15个针,主要用于传输视频,电脑和投影仪可以连接VGA接口、电脑和主机显示屏也可以连接VGA接口。
下图所示是标准的9针接口的串口,串口只能用来传送数据。
5.常见通信接口比较
6.相关术语
同步的通常有时钟线SCL,异步则没有。
7.51单片机的UART
8.串口参数及时序图
因为串口通信时异步通信,没有时钟线来确定什么时候采样,所以通信双方要约定一个通信率。
比特率与波特率不同,比特率描述的是传输多少个位,波特率描述的是传输多少个数据。
奇偶校验是一种比较简单、比较常用的一种校验方法,但是排错率不高。
串口是串行通信,数据一位一位的发送。
9.串口模式图
图示的所有电路结构都在MCU中。串口通过定时器1的溢出率来约定速率,控制收发器的采样时间。
SBUF在等号左边,就是把数据写入SBUF,SBUF在等号右边,就是将SBUF中的数据读出。
TI:发送中断。
RI:接收中断。
10.串口和中断系统
TI和RI占用同一个中断通道,即发送完成和接收完成之后都会进入同一个中断函数,在中断中判断是TI还是RI。
11.串口相关寄存器
需要注意的是,电源控制寄存器的前两位是和串口相关的。
使用串口是主要配置SCON和PCON两个寄存器即可,直接在SBUF中写入数据。
若需要配置中断,则还需要打开EA和ES。
REN=1时允许接收,REN=0时禁止接收。只做单工即数据单向传输时,不需要启动接收使能(REN)。
TI和RI需要重点关注。发送完成后TI会通过硬件置1,需要软件进行复位。RI同理。
12.数据显示模式
HEX模式是底层传输的实际数据,文本模式是将原始数据按照ASCII码表进行编码后的字符。
原始数据只能发送0~255这些数字。
发送字符时也可以用引号引起来。
13.代码示例
(1)串口向电脑发送数据
串口使用定时器1,8位自动重装载模式。
定时器/计数器T1在模式2时一般作为串行通讯时波特率发生器
【补】16位自动重装载,用两个8位的计数器当做一个大的16为计数器,计数范围是0~65535,缺点是每次进入中断时都需要给计数器赋初值,如果不赋初值就会从0开始计数,而且赋初值的语句会占用一定的时间,精度不高。而串口中的传输速率较快,需要精准的时间,使用双8位,8位自动重装载定时器,当溢出时将TH1存放的值自动重装入TL1。
就是16位记的数多,但每次都需要自己写的代码赋初值,浪费时间。双8位就是将16位分开,一个计数,另一个存放初值,每次计数完成后AR会自动将值赋给CNT,不用代码处理,比较快,但只有8位所以记的数少了。
当系统频率选择12MHz时,若波特率太高会产生很大的误差,所以设置为4800,波特率倍速。
波特率加倍是为了分频,如果不加倍时钟会变慢,变慢后就不能匹配了,会形成比较大的误差。
REN=0,SCON=0x40时,接收不使能,REN=1,SCON=0x50时,接收使能。
注意在高系列的单片机中才有AUXR,此处使用的单片机没有AUXR,需要删除相关代码。
上电时需要进行初始化。
当数据接收有误差时,大概率是波特率的问题,可以适当增加延时。波特率越低,通信越稳定,误差影响越小。
main.c文件
//每个1秒发送一个递增的数
#include <REGX52.H>
#include "Delay.h"
#include "UART.h"
unsigned char Sec;
void main()
{
UART_Init(); //串口初始化
while(1)
{
UART_SendByte(Sec); //串口发送一个字节
Sec++; //Sec自增
Delay(1000); //延时1秒
}
}
UART.c文件
#include <REGX52.H>
/**
* @brief 串口初始化,4800bps@12.000MHz
* @param 无
* @retval 无
*/
void UART_Init()
{
SCON=0x40;
PCON |= 0x80; //最高位置1,波特率加倍
TMOD &= 0x0F; //设置定时器模式
TMOD |= 0x20; //设置定时器模式
TL1 = 0xF3; //设定定时初值
TH1 = 0xF3; //设定定时器重装值
ET1 = 0; //禁止定时器1中断
TR1 = 1; //启动定时器1
}
/**
* @brief 串口发送一个字节数据
* @param Byte 要发送的一个字节数据
* @retval 无
*/
void UART_SendByte(unsigned char Byte)
{
SBUF=Byte;
while(TI==0); //检测是否完成,TI=0时一直循环,直到TI=1时跳出循环
TI=0; //手动复位
}
UART.h文件
#ifndef __UART_H__
#define __UART_H__
void UART_Init();
void UART_SendByte(unsigned char Byte);
#endif
Delay.c文件
void Delay(unsigned int xms)
{
unsigned char i, j;
while(xms--)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
}
Delay.h文件
#ifndef __DELAY_H__
#define __DELAY_H__
void Delay(unsigned int xms);
#endif
(2)电脑通过串口控制LED
之前串口发送数据并没有开启中断,只是发完数据后检测TI位是否变为1,然后再置0。
串口接收数据需要一个中断系统是因为,不知道电脑什么时候发送数据,不能一直进行检测,所以利用中断,在电脑发送数据时触发中断,在中断函数中进行数据处理,将数据取出。
【理解】因为发送是你决定什么时候发送,是主动决定的;而接收是被动的,你总不能一直要求单片机时时刻刻准备接收吧。所以把接收当作一种中断请求,它来了我就处理它,没来我就继续干我自己的活。
此时需要将REN接收使能位置1,SCON=0x50。
由于收发数据都会产生中断,所以需要进一步判断。
main.c文件
#include <REGX52.H>
#include "Delay.h"
#include "UART.h"
void main()
{
UART_Init(); //串口初始化
while(1)
{
}
}
void UART_Routine() interrupt 4 //中断服务函数
{
if(RI==1) //如果接收标志位为1,接收到了数据
//接收的同时也在发送,防止发送时也进入中断,所以需要进一步判断
{
P2=~SBUF; //读取数据,取反后输出到LED
UART_SendByte(SBUF); //将受到的数据发回串口
RI=0; //接收标志位清0 //软件进行复位
}
}
UART.c文件
#include <REGX52.H>
/**
* @brief 串口初始化,4800bps@12.000MHz
* @param 无
* @retval 无
*/
void UART_Init()
{
SCON=0x50; //REN=1,接收使能位置1
PCON |= 0x80;
TMOD &= 0x0F; //设置定时器模式
TMOD |= 0x20; //设置定时器模式
TL1 = 0xF3; //设定定时初值
TH1 = 0xF3; //设定定时器重装值
ET1 = 0; //禁止定时器1中断
TR1 = 1; //启动定时器1
EA=1; //打开总中断
ES=1; //打开串口中断
}
/**
* @brief 串口发送一个字节数据
* @param Byte 要发送的一个字节数据
* @retval 无
*/
void UART_SendByte(unsigned char Byte)
{
SBUF=Byte;
while(TI==0);
TI=0;
}
/*串口中断函数模板 //使用时需要移动到主函数中
void UART_Routine() interrupt 4 //中断服务函数
{
if(RI==1) //接收的同时也在发送,防止发送时也进入中断,所以需要进一步判断
{
RI=0; //软件进行复位
}
}
*/
UART.h文件
#ifndef __UART_H__
#define __UART_H__
void UART_Init();
void UART_SendByte(unsigned char Byte);
#endif
Delay.c文件和Delay.h文件同上。
【总结】串口使用过程
初始化→发送→接收(进入中断)
波特率 = 系统时钟频率/(12 * (256 - TH1))
三、LED点阵屏
1.LED点阵屏介绍
2.显示原理
LED点阵屏也有共阴极和共阳极两种接法,如果是单色点阵,共阴和共阳的区别不大,二对于多色点阵,共阴和共阳就有明显区别。
3.74HC595
即使开发板上的64个灯按照行列的扫描方式连接,仍需要16个IO口,由于开发板的IO口很紧张,所以需要对IO口进行扩展。
(不同点阵的排列不同,需要查看数据手册进行对应)
下图中,左侧是移位寄存器,右侧是输出缓存。
SER是串行数据。
上升沿移位的默认状态应该是低电平,但是单片机IO口上电时为高电平,需要手动设置对其进行初始化。
上升沿RCLK为高电平时,左侧的8位数据会直接发送给右侧。
QH'用于级联。 扩展后,输出IO口的速度会被减慢。
先写入高位,再写入低位。
4.开发板引脚对应关系
单片机的IO口输出是弱上拉类型的,弱上拉的特性是输出低电平可以接受很大的电流,输出高电平的电流比较小。低电平强,高电平弱。可以增加一个三极管增强驱动能力,此时IO口不再进行直接驱动,而是作为一个控制信号,驱动VCC是否给LED施加。
5.C51的sfr、sbit
TCON是可位寻址,TMOD是不可位寻址。
不能直接给1和0的使用“&=”,一般用于对某一位进行清零。“|=”一般用于对某一位数据与1相或。“^=”一般用于把某一位数据与1进行异或,不一样则置1,一样则置0,常用于取反。
6.代码示例
(1)LED点阵屏显示图形
要取第八位的数,那么如果Byte也是0x80,& 0x80的话,相当于1&1=1,那么Byte就可以取出第八位,其他为0。
为了保证位对齐,一般对位赋值时,直接给1或0,而对整个寄存器赋值时,一般给0x什么什么。图中所示代码就没有进行位对齐,因为等号后面是个数据,对于位进行赋值时,如果后面给的是数据,那么就相当于给1,“非零即一”,也可以用if语句写。
LED点阵屏同数码管一样,也需要进行消影。
main.c文件
#include <REGX52.H>
#include "Delay.h"
sbit RCK=P3^5; //RCLK
sbit SCK=P3^6; //SRCLK
sbit SER=P3^4; //SER
#define MATRIX_LED_PORT P0
/**
* @brief 74HC595写入一个字节
* @param Byte 要写入的字节
* @retval 无
*/
void _74HC595_WriteByte(unsigned char Byte) //函数名不能以数字开头
{
unsigned char i;
for(i=0;i<8;i++)
{
SER=Byte&(0x80>>i); //0x80,0x40,0x20......
SCK=1; //设置为高电平,上升沿
SCK=0; //设置为低电平,为下一次移位做准备
}
RCK=1;
RCK=0;
}
/**
* @brief LED点阵屏显示一列数据
* @param Column 要选择的列,范围:0~7,0在最左边
* @param Data 选择列显示的数据,高位在上,1为亮,0为灭
* @retval 无
*/
void MatrixLED_ShowColumn(unsigned char Column,Data)
{
_74HC595_WriteByte(Data);
MATRIX_LED_PORT=~(0x80>>Column); //0选中,1不选中
Delay(1); //延时
MATRIX_LED_PORT=0xFF; //位清零
}
void main()
{
SCK=0; //初始化后单片机所有的IO口均为1,需要手动设置进行初始化
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);
}
}
Delay.c文件和Delay.h文件同上。
(2)LED点阵屏显示动画
单片机中有两种存储数据的,一种是程序运行时的暂存器RAM,另一种是放在Flash里的程序存储器。Flash中的存储空间更大。
动画数组中的数据一般不需要改动,放在RAM中可能会很占用内存,所以一般放在Flash中,在数组前加入关键字 code 即可。缺点是数组中的数据不能进行更改。
main.c文件
#include <REGX52.H>
#include "Delay.h"
#include "MatrixLED.h"
//动画数据
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; //Offset偏移量
MatrixLED_Init();
while(1)
{
for(i=0;i<8;i++) //循环8次,显示8列数据
{
MatrixLED_ShowColumn(i,Animation[i+Offset]);
}
Count++; //计次延时
if(Count>15) //每一帧扫描15次
{
Count=0;
Offset+=8; //偏移+8,切换下一帧画面
if(Offset>16) //防止数组溢出
{
Offset=0;
}
}
}
}
MatrixLED.c文件
#include <REGX52.H>
#include "Delay.h"
sbit RCK=P3^5; //RCLK
sbit SCK=P3^6; //SRCLK
sbit SER=P3^4; //SER
#define MATRIX_LED_PORT P0
/**
* @brief 74HC595写入一个字节
* @param Byte 要写入的字节
* @retval 无
*/
void _74HC595_WriteByte(unsigned char Byte)
{
unsigned char i;
for(i=0;i<8;i++)
{
SER=Byte&(0x80>>i);
SCK=1;
SCK=0;
}
RCK=1;
RCK=0;
}
/**
* @brief 点阵屏初始化
* @param 无
* @retval 无
*/
void MatrixLED_Init()
{
SCK=0;
RCK=0;
}
/**
* @brief LED点阵屏显示一列数据
* @param Column 要选择的列,范围:0~7,0在最左边
* @param Data 选择列显示的数据,高位在上,1为亮,0为灭
* @retval 无
*/
void MatrixLED_ShowColumn(unsigned char Column,Data)
{
_74HC595_WriteByte(Data);
MATRIX_LED_PORT=~(0x80>>Column);
Delay(1);
MATRIX_LED_PORT=0xFF;
}
MatrixLED.h
#ifndef __MATRIX_LED_H__
#define __MATRIX_LED_H__
void MatrixLED_Init();
void MatrixLED_ShowColumn(unsigned char Column,Data);
#endif
Delay.c文件和Delay.h文件同上。
四、DS1302实时时钟
1.DS1302介绍
单片机定时器计时存在几个缺点:
- 精度不高
- 利用定时器计时会占用CPU的时间
- 单片机定时器的时钟不能掉电继续运行
对于DS1302这个实时时钟芯片,会带有一个备用电池,如果掉电的话,芯片内部的逻辑判断会自动把电源切换到备用电池,即使单片机不工作,备用电池也会给时钟芯片提供电能保证其能够持续计时。
其他的时钟芯片还有:
DS3231,特点是精度很高,集成度更高,价格更贵。
DS12C887,特点是该芯片内部自带电池,不需要外接。
当我们拿到一个芯片或者根据需求找到一款芯片时
第一步是要了解它的功能,将芯片的功能和需求进行对应;
第二步是找到芯片的数据
2.引脚定义和应用电路
DIP——直插封装;SO——贴片封装。
右图是应用电路。
VCC2是主电源,与单片机电源的正极连在一起。VCC1连接备用电池,备用电池的正极连接VCC1,负极连接GND。由于该芯片具有涓细电流充电能力,所以在连接VCC时,会对备用电池进行充电,一旦VCC断开,会继续利用备用电池给芯片进行供电,保证了芯片的持续工作。
一般情况下不需要配置涓细电流充电能力,因为芯片在掉电的情况下使用备用电池的功耗是很低的。
根据原理图可以看到,开发板上的时钟芯片是没有接备用电池的,所以在该开发板上单片机掉电之后时钟芯片会停止工作。
X1和X2之间接一个固定频率的晶振,晶振的作用是为了给实时时钟系统提供一个稳定的计数脉冲,晶振经过内部电路的处理,会输出一个1Hz的标准的频率,而且频率的精度很高。
一般来说,晶振和时钟稳定的脉冲有关,而且晶振的振荡器稳定性特别高,产生的时钟频率精度很高,它的全称是石英晶体振荡器。
左侧的三个引脚是通信引脚,利用这三个引脚,通过专用的通信协议,单片机可以把芯片内部的时钟读取出来,并将设置的时间进行写入。这三个引脚的操作方式和74HC595移位寄存器十分类似,也是上升沿进行移位。
3.内部结构框图
芯片内部的时间都存储在实时时钟的寄存器中,寄存器的大小为31×8 RAM,这里的RAM与单片机中的RAM相同,例如定义一个变量 unsigned char i, 就相当于在单片机内部的寄存器中,开辟了一个地址的空间,这个空间的名字叫 i 。
CE引脚负责芯片使能(即使不使能时钟也是工作的),主要是用来判断的,IO口和SCLK相当于输入移位寄存器,将数据进行串行传输交互,CE为高电平时,I/O和SCLK才有效。
4.寄存器定义
下图所示的是与时钟有关的寄存器,芯片内部有很多的变量,变量的地址和内容都是定义好的。
这些寄存器都有相应的地址,每个地址下有一个数据,数据是一个字节一个字节的存储的,一个字节有8位,地址0存储的是秒寄存器,Day寄存器代表是星期。
WP是写保护,置1时,写入操作无效,但是可以读出。最后一个寄存器是存储涓细电流充电的寄存器,在该开发板上不需要配置。
表2是地址命令字,芯片需要对寄存器进行读写,需要完成几个任务:在哪写入什么,在哪读出(什么)。其中“在哪”体现在地址上,“写入”还是“读出”,地址命令字主要负责在哪写入、在哪读出。
命令字是一个字节,有8位,最高位7固定为1,第6位操作RAM给1,操作CK给0,第5位到地1位是地址位,A4~A0,(81h是1000 0001,而1000 0001中五到一位是00000,这个就是秒的地址),第0位用来确定是读还是写,1读0写。
CH是时钟停止,时钟暂停的意思,如果BIT7置一,则整个时钟停止计时。
5.时序定义
在整个写入和读取的过程中CE始终保持高电平,SCLK负责提供固定的时钟,I/O给数据。在SCLK的时钟上升沿,I/O的数据会被写入,在下降沿,DS1302会把数据进行输出,简单来说就是,上升沿单片机向时钟芯片写入数据,下降沿时钟芯片向单片机写入数据即读出时钟芯片的数据。这个协议和串口通信那里提到的SPI协议类似。
只有读出数据的D0~D7是时钟芯片控制的,其他的都是单片机进行控制的。
I/O扣罚送两个字节,第一个字节是命令字,第二个字节是数据。这里的移位寄存器先发送最低位。
6.BCD码
芯片内部寄存器的数据不是以二进制进行存储的,而是以BCD码的形式进行存储的。
BCD码在操作的时候不方便,它将十位和个位分开了,如果想要进行判断,需要单独判断,但是BCD码的好处是,给数码管译码的时候很方便。
7.代码示例
(1)DS1302时钟
因为时钟需要通过LCD1602进行显示,所以需要提前加入LCD1602.c文件和LCD1602.h文件。
在对SCLK操作时,先置1后又直接置0,这里的时序操作需要考虑时钟芯片能够承受的时钟最快频率,如果时钟线操作太快,时钟芯片可能反应不过来。可以在数据手册中查询操作的时序时间。因为51单片机的操作速率比较慢,所以可以不加延时,如果单片机的速度很快,超过了限定的范围,就需要加入延时。
因为DS1302_IO是位变量,与位有关的是逻辑判断,非0即真。
根据时序图可知,单字节写入的函数有16个脉冲,而单字节读取的函数有15个脉冲。在单字节读取函数中,DS1302_SCLK先给0再给1。
先0后1每次循环后就会在凸处停下,先1后0每次循环后会在凹处停下。
对于单字节读取函数,单片机在传输万第一个字节后,需要把I/O的控制权转让给DS1302,先给下降沿再给上升沿后,时序到了如图所示的位置上。
局部变量刚定义之后的初始值不一定为0,需要赋值,全局变量定义之后的默认初始值为0。
一般情况下,与&用来清零,或|用来置一。
这里重复置一的目的是去掉一个脉冲。
如果声明外部数组和变量,前面必须加 extern ,加extern表示是变量声明,否则是全局变量的定义。
main.c文件
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
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);//显示秒
}
}
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
//时间数组,索引0~6分别为年、月、日、时、分、秒、星期
unsigned char DS1302_Time[]={19,11,16,12,59,55,6};
/**
* @brief DS1302初始化
* @param 无
* @retval 无
*/
void DS1302_Init(void) //初始化
{
DS1302_CE=0;
DS1302_SCLK=0;
}
/**
* @brief DS1302写一个字节
* @param Command 命令字/地址
* @param Data 要写入的数据
* @retval 无
*/
void DS1302_WriteByte(unsigned char Command,Data) //单字节写入
{
unsigned char i;
DS1302_CE=1;
for(i=0;i<8;i++) //命令字低位在前
{
DS1302_IO=Command&(0x01<<i); //取出第i位
DS1302_SCLK=1;
DS1302_SCLK=0;
}
for(i=0;i<8;i++)
{
DS1302_IO=Data&(0x01<<i);
DS1302_SCLK=1;
DS1302_SCLK=0;
}
DS1302_CE=0;
}
/**
* @brief DS1302读一个字节
* @param Command 命令字/地址
* @retval 读出的数据
*/
unsigned char DS1302_ReadByte(unsigned char Command) //单字节读取
{
unsigned char i,Data=0x00;
Command|=0x01; //将指令转换为读指令,将最低位置1
DS1302_CE=1;
for(i=0;i<8;i++)
{
DS1302_IO=Command&(0x01<<i);
DS1302_SCLK=0;
DS1302_SCLK=1;
}
for(i=0;i<8;i++)
{
DS1302_SCLK=1; //重复置一
DS1302_SCLK=0;
if(DS1302_IO){Data|=(0x01<<i);} //如果端口输出为1,就把Data的值读取出来了
}
DS1302_CE=0;
DS1302_IO=0; //读取后将IO设置为0,否则读出的数据会出错
return Data;
}
/**
* @brief DS1302设置时间,调用之后,DS1302_Time数组的数字会被设置到DS1302中
* @param 无
* @retval 无
*/
void DS1302_SetTime(void)
{
DS1302_WriteByte(DS1302_WP,0x00); //关闭写保护
DS1302_WriteByte(DS1302_YEAR,DS1302_Time[0]/10*16+DS1302_Time[0]%10);//十进制转BCD码后写入
DS1302_WriteByte(DS1302_MONTH,DS1302_Time[1]/10*16+DS1302_Time[1]%10);
DS1302_WriteByte(DS1302_DATE,DS1302_Time[2]/10*16+DS1302_Time[2]%10);
DS1302_WriteByte(DS1302_HOUR,DS1302_Time[3]/10*16+DS1302_Time[3]%10);
DS1302_WriteByte(DS1302_MINUTE,DS1302_Time[4]/10*16+DS1302_Time[4]%10);
DS1302_WriteByte(DS1302_SECOND,DS1302_Time[5]/10*16+DS1302_Time[5]%10);
DS1302_WriteByte(DS1302_DAY,DS1302_Time[6]/10*16+DS1302_Time[6]%10);
DS1302_WriteByte(DS1302_WP,0x80); //打开写保护
}
/**
* @brief DS1302读取时间,调用之后,DS1302中的数据会被读取到DS1302_Time数组中
* @param 无
* @retval 无
*/
void DS1302_ReadTime(void)
{
unsigned char Temp; //暂时
Temp=DS1302_ReadByte(DS1302_YEAR);
DS1302_Time[0]=Temp/16*10+Temp%16;//BCD码转十进制后读取
Temp=DS1302_ReadByte(DS1302_MONTH);
DS1302_Time[1]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_DATE);
DS1302_Time[2]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_HOUR);
DS1302_Time[3]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_MINUTE);
DS1302_Time[4]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_SECOND);
DS1302_Time[5]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_DAY);
DS1302_Time[6]=Temp/16*10+Temp%16;
}
DS1302.h文件
#ifndef __DS1302_H__
#define __DS1302_H__
//外部可调用时间数组,索引0~6分别为年、月、日、时、分、秒、星期
extern unsigned char DS1302_Time[];
void DS1302_Init(void);
void DS1302_WriteByte(unsigned char Command,Data);
unsigned char DS1302_ReadByte(unsigned char Command);
void DS1302_SetTime(void);
void DS1302_ReadTime(void);
#endif
(2)DS1302可调时钟
因为时钟需要通过按键进行调整,所以需要提前加入Key.c文件、Key.h文件以及Timer0.c文件、Timer0.h文件。
越界清零的几种写法
无符号的数据从0再减1,会变成255,不存在负数。所以可以改为有符号数,范围是-128~127。
main.c文件
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
#include "Key.h"
#include "Timer0.h"
unsigned char KeyNum,MODE,TimeSetSelect,TimeSetFlashFlag;
void TimeShow(void)//时间显示功能
{
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);//显示秒
}
void TimeSet(void)//时间设置功能
{
if(KeyNum==2)//按键2按下
{
TimeSetSelect++;//设置选择位加1
TimeSetSelect%=6;//越界清零
}
if(KeyNum==3)//按键3按下
{
DS1302_Time[TimeSetSelect]++;//时间设置位数值加1
if(DS1302_Time[0]>99){DS1302_Time[0]=0;}//年越界判断
if(DS1302_Time[1]>12){DS1302_Time[1]=1;}//月越界判断
if( DS1302_Time[1]==1 || DS1302_Time[1]==3 || DS1302_Time[1]==5 || DS1302_Time[1]==7 ||
DS1302_Time[1]==8 || DS1302_Time[1]==10 || DS1302_Time[1]==12)//日越界判断
{
if(DS1302_Time[2]>31){DS1302_Time[2]=1;}//大月
}
else if(DS1302_Time[1]==4 || DS1302_Time[1]==6 || DS1302_Time[1]==9 || DS1302_Time[1]==11)
{
if(DS1302_Time[2]>30){DS1302_Time[2]=1;}//小月
}
else if(DS1302_Time[1]==2)
{
if(DS1302_Time[0]%4==0)
{
if(DS1302_Time[2]>29){DS1302_Time[2]=1;}//闰年2月
}
else
{
if(DS1302_Time[2]>28){DS1302_Time[2]=1;}//平年2月
}
}
if(DS1302_Time[3]>23){DS1302_Time[3]=0;}//时越界判断
if(DS1302_Time[4]>59){DS1302_Time[4]=0;}//分越界判断
if(DS1302_Time[5]>59){DS1302_Time[5]=0;}//秒越界判断
}
if(KeyNum==4)//按键3按下
{
DS1302_Time[TimeSetSelect]--;//时间设置位数值减1
if(DS1302_Time[0]<0){DS1302_Time[0]=99;}//年越界判断
if(DS1302_Time[1]<1){DS1302_Time[1]=12;}//月越界判断
if( DS1302_Time[1]==1 || DS1302_Time[1]==3 || DS1302_Time[1]==5 || DS1302_Time[1]==7 ||
DS1302_Time[1]==8 || DS1302_Time[1]==10 || DS1302_Time[1]==12)//日越界判断
{
if(DS1302_Time[2]<1){DS1302_Time[2]=31;}//大月
if(DS1302_Time[2]>31){DS1302_Time[2]=1;}
}
else if(DS1302_Time[1]==4 || DS1302_Time[1]==6 || DS1302_Time[1]==9 || DS1302_Time[1]==11)
{
if(DS1302_Time[2]<1){DS1302_Time[2]=30;}//小月
if(DS1302_Time[2]>30){DS1302_Time[2]=1;}
}
else if(DS1302_Time[1]==2)
{
if(DS1302_Time[0]%4==0)
{
if(DS1302_Time[2]<1){DS1302_Time[2]=29;}//闰年2月
if(DS1302_Time[2]>29){DS1302_Time[2]=1;}
}
else
{
if(DS1302_Time[2]<1){DS1302_Time[2]=28;}//平年2月
if(DS1302_Time[2]>28){DS1302_Time[2]=1;}
}
}
if(DS1302_Time[3]<0){DS1302_Time[3]=23;}//时越界判断
if(DS1302_Time[4]<0){DS1302_Time[4]=59;}//分越界判断
if(DS1302_Time[5]<0){DS1302_Time[5]=59;}//秒越界判断
}
//更新显示,根据TimeSetSelect和TimeSetFlashFlag判断可完成闪烁功能
if(TimeSetSelect==0 && TimeSetFlashFlag==1){LCD_ShowString(1,1," ");}
else {LCD_ShowNum(1,1,DS1302_Time[0],2);}
if(TimeSetSelect==1 && TimeSetFlashFlag==1){LCD_ShowString(1,4," ");}
else {LCD_ShowNum(1,4,DS1302_Time[1],2);}
if(TimeSetSelect==2 && TimeSetFlashFlag==1){LCD_ShowString(1,7," ");}
else {LCD_ShowNum(1,7,DS1302_Time[2],2);}
if(TimeSetSelect==3 && TimeSetFlashFlag==1){LCD_ShowString(2,1," ");}
else {LCD_ShowNum(2,1,DS1302_Time[3],2);}
if(TimeSetSelect==4 && TimeSetFlashFlag==1){LCD_ShowString(2,4," ");}
else {LCD_ShowNum(2,4,DS1302_Time[4],2);}
if(TimeSetSelect==5 && TimeSetFlashFlag==1){LCD_ShowString(2,7," ");}
else {LCD_ShowNum(2,7,DS1302_Time[5],2);}
}
void main()
{
LCD_Init();
DS1302_Init();
Timer0Init();
LCD_ShowString(1,1," - - ");//静态字符初始化显示
LCD_ShowString(2,1," : : ");
DS1302_SetTime();//设置时间
while(1)
{
KeyNum=Key();//读取键码
if(KeyNum==1)//按键1按下
{
if(MODE==0){MODE=1;TimeSetSelect=0;}//功能切换
else if(MODE==1){MODE=0;DS1302_SetTime();}
}
switch(MODE)//根据不同的功能执行不同的函数
{
case 0:TimeShow();break;
case 1:TimeSet();break;
}
}
}
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count;
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++;
if(T0Count>=500)//每500ms进入一次
{
T0Count=0;
TimeSetFlashFlag=!TimeSetFlashFlag;//闪烁标志位取反
}
}
五、蜂鸣器
1.蜂鸣器介绍
有源蜂鸣器和无源蜂鸣器的外观相差不大,但是驱动方式有很大区别。
有源蜂鸣器的优点是驱动简单,缺点是频率固定。
2.驱动电路
最常见的驱动电路是三极管开关驱动电路,NPN型是高电平导通的三极管开关,PNP型是低电平导通的三极管开关。在基极需要接一个限流电阻,减小控制信号的电流,相当于弱化了控制信号的驱动能力,实际提供驱动能力的是VCC。
对于数字电路来说,限流电阻只需要保证三极管能够饱和即可,1kΩ和10kΩ都可。
因为该单片机的I/O口不能直接驱动蜂鸣器,需要经过一个芯片ULN2003。
3.ULN2003
达林顿晶体管也是一种晶体管开关,达林顿管也称复合管,可以增大单片机的驱动能力。
逻辑框图中的非门实际上是由达林顿管组成的,具有很强的驱动能力。
给0的时候,虽然输出为1,但是不具有驱动能力,相当于断开。给1时才驱动。
二极管是用来测试的,或者说是继电器的续流二极管,防止反向开合时产生高压脉冲。
ULN2003常用于步进电机的驱动,该单片机中也用来驱动步进电机,不过驱动步进电机只需要四路,多出来的三路可以选一路用来驱动蜂鸣器。
【注意】无源蜂鸣器必须给交流振荡才能发声,不能一直通电,无源蜂鸣器内部的线圈电阻很小,如果一直通电很容易造成蜂鸣器的烧毁。
4.键盘与音符对照
每个组的同一个音相差八度,没两个相邻的键相差半音,两个相邻的半音相差全音。
#是升音符号,b是降音符号。
下图中依次为全音符、二分音符、四分音符、八分音符、十六分音符、三十二分音符。
我们一般以四分音符作为时间基准,这个基准可以使100ms,也可以是200ms,根据节奏的快慢定。
附点表示该音符延长原来时长的1/2。
是延音符。
5.音符与频率对照
根据频率值控制定时器,产生相应频率的计时,有了频率就有了计时的频率,有了周期就可以控制中断,再控制I/O口的翻转,进而控制频率。
我们选择低音6作为基准频率
他们之间的12个键是以等比数列进行平分的,也称十二平均律。
利用定时器中断产生频率,定时器中断周期的时间是通过定时器TL0和TH0的重装值确定的。
单片机中不容易产生频率,更容易产生周期,所以应该先将频率计算为周期。
这里使用的单片机是12T的,机器周期是振荡周期的1/12,振荡周期就是晶振,因为晶振是12MHz,所以机器周期为1MHz,每经过一个机器周期,定时器的计数器加一,耗时1us。如果晶振不是12MHz,例如为11.0592MHz,则定时的时间不是1us,机器周期的频率=11.0592/12,11.0592MHz加一的时间=1/机器周期的频率。
I/O口翻转两次为一个周期。将定时器的周期设置为我们想要的声音周期的两倍。翻转频率是自身的二倍,原来一周期内翻转两次,现在半个周期翻转两次,所以是自身的二倍。除以2就是算出接通到断开的时间,为周期的一半,不同频率接通到断开的时间不同,振荡周期不同,频率也会不同。
相当于你要得到两个1ms的高电平,首先要把IO口从0置1,维持1ms,再把IO口从1置0,维持1ms,再置1,维持1ms再置0,维持1ms,相当于做了4次IO口反转,用了4ms才有两个1ms高电平。
TH0=重装载值的高八位,TL0=重装载值的低八位,再控制定时器中断,在中的中翻转I/O口即可产生对应的频率。
6.简谱
7.代码示例
(1)蜂鸣器播放提示音
因为需要根据独立按键输入,数码管显示1、2、3、4,所以需要提前加入Delay.c文件、Delay.h文件、Key.c文件、Key.h文件、Nixie.c文件、Nixie.h文件。
注意这里要将数码管Nixie.c文件中的后两句注释掉,因为这里是静态显示,不需要清零。
如果延时中出现_nop_,则需要添加头文件
for循环是让时间保持1s的,延时函数改变成0.5us,那要保持时长仍为1s,肯定是要乘以2的。
main.c文件
#include <REGX52.H>
#include "Delay.h"
#include "Key.h"
#include "Nixie.h"
#include "Buzzer.h"
unsigned char KeyNum;
void main()
{
Nixie(1,0); //为了保证数码管上电时显示0
while(1)
{
KeyNum=Key(); //获取键码值
if(KeyNum)
{
Buzzer_Time(100);
Nixie(1,KeyNum);
}
}
}
Buzzer.c文件
#include <REGX52.H>
#include <INTRINS.H>
//蜂鸣器端口:
sbit Buzzer=P1^5;
/**
* @brief 蜂鸣器私有延时函数,延时500us
* @param 无
* @retval 无
*/
void Buzzer_Delay500us() //@12.000MHz
{
unsigned char i;
_nop_(); //延时1us
i = 247;
while (--i);
}
/**
* @brief 蜂鸣器发声
* @param ms 发声的时长,范围:0~32767
* @retval 无
*/
void Buzzer_Time(unsigned int ms)
{
unsigned int i;
for(i=0;i<ms*2;i++) //for循环是让时间保持1s的,延时函数改变成0.5us,那要保持时长仍为1s,肯定是要乘以2的
{
Buzzer=!Buzzer; //翻转
Buzzer_Delay500us(); //延时,实现500us翻转一次
}
}
Buzzer.h文件
#ifndef __BUZZER_H__
#define __BUZZER_H__
void Buzzer_Time(unsigned int ms);
#endif
(2)蜂鸣器播放音乐
需要提前加入Delay.c文件、Delay.h文件、Timer0.c文件、Timer0.h文件。
main.c文件
#include <REGX52.H>
#include "Delay.h"
#include "Timer0.h"
//蜂鸣器端口定义
sbit Buzzer=P1^5;
//播放速度,值为四分音符的时长(ms)
#define SPEED 500
//音符与索引对应表,P:休止符,L:低音,M:中音,H:高音,下划线:升半音符号#
#define P 0
#define L1 1
#define L1_ 2
#define L2 3
#define L2_ 4
#define L3 5
#define L4 6
#define L4_ 7
#define L5 8
#define L5_ 9
#define L6 10
#define L6_ 11
#define L7 12
#define M1 13
#define M1_ 14
#define M2 15
#define M2_ 16
#define M3 17
#define M4 18
#define M4_ 19
#define M5 20
#define M5_ 21
#define M6 22
#define M6_ 23
#define M7 24
#define H1 25
#define H1_ 26
#define H2 27
#define H2_ 28
#define H3 29
#define H4 30
#define H4_ 31
#define H5 32
#define H5_ 33
#define H6 34
#define H6_ 35
#define H7 36
//索引与频率对照表
unsigned int FreqTable[]={
0,
63628,63731,63835,63928,64021,64103,64185,64260,64331,64400,64463,64528,
64580,64633,64684,64732,64777,64820,64860,64898,64934,64968,65000,65030,
65058,65085,65110,65134,65157,65178,65198,65217,65235,65252,65268,65283,
};
//乐谱
unsigned char code Music[]={
//音符,时值,
//1
P, 4,
P, 4,
P, 4,
M6, 2,
M7, 2,
H1, 4+2,
M7, 2,
H1, 4,
H3, 4,
M7, 4+4+4,
M3, 2,
M3, 2,
//2
M6, 4+2,
M5, 2,
M6, 4,
H1, 4,
M5, 4+4+4,
M3, 4,
M4, 4+2,
M3, 2,
M4, 4,
H1, 4,
//3
M3, 4+4,
P, 2,
H1, 2,
H1, 2,
H1, 2,
M7, 4+2,
M4_,2,
M4_,4,
M7, 4,
M7, 8,
P, 4,
M6, 2,
M7, 2,
//4
H1, 4+2,
M7, 2,
H1, 4,
H3, 4,
M7, 4+4+4,
M3, 2,
M3, 2,
M6, 4+2,
M5, 2,
M6, 4,
H1, 4,
//5
M5, 4+4+4,
M2, 2,
M3, 2,
M4, 4,
H1, 2,
M7, 2+2,
H1, 2+4,
H2, 2,
H2, 2,
H3, 2,
H1, 2+4+4,
//6
H1, 2,
M7, 2,
M6, 2,
M6, 2,
M7, 4,
M5_,4,
M6, 4+4+4,
H1, 2,
H2, 2,
H3, 4+2,
H2, 2,
H3, 4,
H5, 4,
//7
H2, 4+4+4,
M5, 2,
M5, 2,
H1, 4+2,
M7, 2,
H1, 4,
H3, 4,
H3, 4+4+4+4,
//8
M6, 2,
M7, 2,
H1, 4,
M7, 4,
H2, 2,
H2, 2,
H1, 4+2,
M5, 2+4+4,
H4, 4,
H3, 4,
H3, 4,
H1, 4,
//9
H3, 4+4+4,
H3, 4,
H6, 4+4,
H5, 4,
H5, 4,
H3, 2,
H2, 2,
H1, 4+4,
P, 2,
H1, 2,
//10
H2, 4,
H1, 2,
H2, 2,
H2, 4,
H5, 4,
H3, 4+4+4,
H3, 4,
H6, 4+4,
H5, 4+4,
//11
H3, 2,
H2, 2,
H1, 4+4,
P, 2,
H1, 2,
H2, 4,
H1, 2,
H2, 2+4,
M7, 4,
M6, 4+4+4,
P, 4,
0xFF //终止标志
};
unsigned char FreqSelect,MusicSelect;
void main()
{
Timer0Init();
while(1)
{
if(Music[MusicSelect]!=0xFF) //如果不是停止标志位
{
FreqSelect=Music[MusicSelect]; //选择音符对应的频率
MusicSelect++;
Delay(SPEED/4*Music[MusicSelect]); //选择音符对应的时值
MusicSelect++;
TR0=0;
Delay(5); //音符间短暂停顿
TR0=1;
}
else //如果是停止标志位
{
TR0=0;
while(1);
}
}
}
void Timer0_Routine() interrupt 1 //该函数1ms进入一次,1ms翻转一次,2ms一个周期,对应1/0.002=500Hz
{
if(FreqTable[FreqSelect]) //如果不是休止符
{
/*取对应频率值的重装载值到定时器*/
TL0 = FreqTable[FreqSelect]%256; //设置定时初值
TH0 = FreqTable[FreqSelect]/256; //设置定时初值
Buzzer=!Buzzer; //翻转蜂鸣器IO口
}
}