【51单片机学习】定时器、串口、LED点阵屏、DS1302实时时钟、蜂鸣器

一、定时器

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流水灯模式

cs 复制代码
#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)定时器时钟

注意:中断函数中不能执行过长的任务。

cs 复制代码
#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文件
cs 复制代码
//每个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文件
cs 复制代码
#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文件
cs 复制代码
#ifndef __UART_H__
#define __UART_H__

void UART_Init();
void UART_SendByte(unsigned char Byte);

#endif
Delay.c文件
cs 复制代码
void Delay(unsigned int xms)
{
	unsigned char i, j;
	while(xms--)
	{
		i = 2;
		j = 239;
		do
		{
			while (--j);
		} while (--i);
	}
}
Delay.h文件
cs 复制代码
#ifndef __DELAY_H__
#define __DELAY_H__

void Delay(unsigned int xms);

#endif

(2)电脑通过串口控制LED

之前串口发送数据并没有开启中断,只是发完数据后检测TI位是否变为1,然后再置0。

串口接收数据需要一个中断系统是因为,不知道电脑什么时候发送数据,不能一直进行检测,所以利用中断,在电脑发送数据时触发中断,在中断函数中进行数据处理,将数据取出。

【理解】因为发送是你决定什么时候发送,是主动决定的;而接收是被动的,你总不能一直要求单片机时时刻刻准备接收吧。所以把接收当作一种中断请求,它来了我就处理它,没来我就继续干我自己的活。

此时需要将REN接收使能位置1,SCON=0x50。

由于收发数据都会产生中断,所以需要进一步判断。

main.c文件
cs 复制代码
#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文件
cs 复制代码
#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文件
cs 复制代码
#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文件
cs 复制代码
#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文件
cs 复制代码
#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文件
cs 复制代码
#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
cs 复制代码
#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介绍

单片机定时器计时存在几个缺点:

  1. 精度不高
  2. 利用定时器计时会占用CPU的时间
  3. 单片机定时器的时钟不能掉电继续运行

对于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文件
cs 复制代码
#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文件
cs 复制代码
#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文件
cs 复制代码
#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文件
cs 复制代码
#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文件
cs 复制代码
#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文件
cs 复制代码
#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文件
cs 复制代码
#ifndef __BUZZER_H__
#define __BUZZER_H__

void Buzzer_Time(unsigned int ms);

#endif

(2)蜂鸣器播放音乐

需要提前加入Delay.c文件Delay.h文件Timer0.c文件Timer0.h文件

main.c文件
cs 复制代码
#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口
	}
}
相关推荐
汇能感知4 小时前
摄像头模块在运动相机中的特殊应用
经验分享·笔记·科技
阿巴Jun4 小时前
【数学】线性代数知识点总结
笔记·线性代数·矩阵
伴杯猫4 小时前
【ESP32-IDF】基础外设开发2:系统中断矩阵
c语言·单片机·嵌入式硬件·mcu·物联网·github
茯苓gao5 小时前
STM32G4 速度环开环,电流环闭环 IF模式建模
笔记·stm32·单片机·嵌入式硬件·学习
是誰萆微了承諾5 小时前
【golang学习笔记 gin 】1.2 redis 的使用
笔记·学习·golang
DKPT6 小时前
Java内存区域与内存溢出
java·开发语言·jvm·笔记·学习
aaaweiaaaaaa6 小时前
HTML和CSS学习
前端·css·学习·html
ST.J6 小时前
前端笔记2025
前端·javascript·css·vue.js·笔记
Suckerbin6 小时前
LAMPSecurity: CTF5靶场渗透
笔记·安全·web安全·网络安全
点灯小铭6 小时前
基于STM32单片机的智能粮仓温湿度检测蓝牙手机APP设计
stm32·单片机·智能手机·毕业设计·课程设计