C51单片机学习笔记–DS1302时钟芯片&&蜂鸣器&&I2C总线&&AT24C02存储器
DS1302时钟芯片是具有涓细电流充电能力的低功耗实时时钟芯片。实时时钟计算年、月、日、时、分、秒、星期,直到 2100 年,并有闰年调节功能。DS1302芯片包含一个用于存储实时时钟/日历的 31 字节的静态 RAM,可通过简单的串行接口与微处理器通讯,将当前的是时钟存于RAM。相较于定时器,时钟芯片的精度更高,运行更稳定且DS1302芯片对于少于 31 天的月份月末会自动调整,并会自动对闰年进行校正。
DS1302时钟芯片由VCC和GND端供电,晶振通过内部电路处理后向时钟芯片提供一个稳定的脉冲,时钟芯片利用该脉冲来计时。CE引脚作用是输出使能电平来控制数据输入输出和串行时钟,IO引脚则用来传输数据,写程序时的数据传输过程类似74HC595移位寄存器,串行时钟(SCLK)也是通过高低电平来控制数据传输。
DS1302工作时为了对任何数据传送进行初始化,需要将复位脚(RST)置为高电平且将8位地址和命令符分别装入移位寄存器。数据在时钟(SCLK)的上升沿开始串行输入,前8位指定访问地址,命令符装入移位寄存器后,在之后的时钟周期,写操作时输入数据,读操作时输出数据。
上面讲到,通过控制DS1302相关寄存器,我们就可以让CPU和时钟芯片数据传输,从而获取时钟时间状态以及控制时钟的初始值。
从上述图中可以看出,通过控制寄存器IO引脚传输数据时要同时输入命令字和数据,其中读写命令字分别有9个,分别对应时钟芯片不同的计时单位,所以通过相应的命令字和初始值我们就可以初始化时钟的某个计时单位。
通过上述数据传输原理图,我们要先给CE引脚赋高电平来打开数据传输读写口,然后通过给SCLK串行时钟引脚间隔赋高低电平来完成单字节写入,原理类似移位寄存器,在高电平时将二进制数一位一位挪入,最后给CE引脚赋低电平来关闭数据传输读写口,读出的过程也类似。
按照上述原理,我们先给DS1302时钟芯片数据传输过程进行封装,然后在主函数里面调用读和写的子函数来实现时钟的初始化。
这里我们也会用到LCD1602液晶屏作为时钟的显示模块,所以记得在主函数之前include一下LCD1602的模块。
# include
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_Write(unsigned char Command, Data)//定义一个写入函数
{
unsigned int i=0;
DS1302_CE=1;
for (i=0;i<8;i++)
{
DS1302_IO=Command&(0x01<<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;
}
unsigned char DS1302_Read(unsigned char Command)//定义一个读出函数
{
unsigned int i=0,Data=0x00;
Command|=0x01;
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);}
}
DS1302_CE=0;
DS1302_IO=0;
return Data;
}
这个是封装好的DS1302模块,开始前先进行三个数据传输引脚的位声明。
# include
# include "LCD1602.h"
# include "DS1302.h"
# include "delay.h"
void main()
{
unsigned char Second=0,Minute=0,Hour=0,Date=0,Month=0;
LCD_Init();
DS1302_Init();
LCD_ShowString(1,1,"Timer: Date:");
LCD_ShowString(2,1," : : .");
DS1302_Write(0x8E,0x00);
DS1302_Write(0x80,50/10*16+50%10);
DS1302_Write(0x82,4/10*16+4%10);
DS1302_Write(0x84,10/10*16+10%10);
DS1302_Write(0x86,13/10*16+13%10);
DS1302_Write(0x88,11/10*16+11%10);
DS1302_Write(0x8E,0x80);
while(1)
{
Second=DS1302_Read(0x81)/16*10+DS1302_Read(0x81)%16;
Minute=DS1302_Read(0x83)/16*10+DS1302_Read(0x83)%16;
Hour=DS1302_Read(0x85)/16*10+DS1302_Read(0x85)%16;
Date=DS1302_Read(0x87)/16*10+DS1302_Read(0x87)%16;
Month=DS1302_Read(0x89)/16*10+DS1302_Read(0x89)%16;
LCD_ShowNum(2,7,Second,2);
LCD_ShowNum(2,4,Minute,2);
LCD_ShowNum(2,1,Hour,2);
LCD_ShowNum(2,11,Month,2);
LCD_ShowNum(2,14,Date,2);
}
}
主函数里面除了调用时钟读写模块,还要进行BCD转码,否则会出错。
蜂鸣器分为有源蜂鸣器和无源蜂鸣器。
这里的“源”不是指电源。而是指震荡源。 也就是说,有源蜂鸣器内部带震荡源,所以只要一通电就会叫。 而无源内部不带震荡源,所以如果用直流信号无法令其鸣叫,必须用2K~5K的方波去驱动它。
通过改变引脚输出的频率,就可以调整蜂鸣器的音调,产生各种不同的声音;同时改变输出电平的高低电平占空比,则可以控制蜂鸣器的声音大小。
上面的截图显示了从低音,中音到高音的每个音符对应的频率,其中包括7个基本音符的升调,通过频率我们可以计算出对应的周期,对周期取整后就可以近似得到蜂鸣器发出该声调所需要的高低电平时间。
我们可以利用之前学过的中断定时器原理,通过设定定时器的重装载值来控制中断系统在一个设定的时间后产生一个中断,此时翻转蜂鸣器就可以发出声音了。
I2C(Inter-Integrated Circuit)总线是由PHILIPS公司开发的两线式串行通信总线,是微电子通信控制领域广泛采用的一种总线标准。I2C总线可以将单片机与其他具有I2C总线通信接口的外围设备连接起来,通过串行数据(SDA)线和串行时钟(SCL)线与连接到该双线的器件传递信息。每个I2C器件都有一个唯一的识别地址(I2C总线支持7位和10位地址),而且可以作为一个发送器或接收器。I2C器件在执行数据传输时也可以看作是主机或从机,主机是初始化总线数据传输并产生允许传输时钟信号的器件,此时任何被寻址的其他I2C器件都被认为是从机。
下面讲讲I2C总线的6种时序结构,用于主机和从机的数据传输。
根据上述原理图,写程序时我们要建立六个子函数模块,开始传输数据时SCL和SDA都应该处于高电平,先拉低SDA电平再拉低SCL电平,在传输或者接收数据时要拉高并保持SCL高电平然后读取放到SDA上的数据,结束传输数据就和开始时相反,还要设置应答模块在主机和从机间建立应答通讯。
AT24C02是一种掉电数据不丢失的储存器,通过I2C总线的通讯协议来给存储器写入数据,但AT24C02内部储存空间只有256字节,每个字节可以存入8位二进制数,所以每个字节所存的最大整数为255,存储和读取时通过地址来找数据,类似C语言里面的指针。
从上述原理图可以看出,AT24C02分别有8个引脚,其中A2、 A1、 A0这三个引脚用于器件地址的输入,SDA是双向串行数据传输引脚,SCL则是串行时钟输入引脚,数据上升沿写入下降沿读出,WP是写保护引脚,专门为数据提供保护,VCC和GND则是电源正负极。
结合I2C总线是时序结构和数据帧以及上述原理,我们只需要将AT24C02封装成一个包含读写函数的模块即可,读写模块分别遵照数据读写时发送地址和应答,再发送数据和应答的基本原理并调用之前的I2C模块函数。
细心的小伙伴肯定会发现缺了关于蜂鸣器的实验,别着急这不来了!
之前学到蜂鸣器时我的脑子里就蹦出来一个神奇的想法,可不可以做一个类似电子琴的玩意呢?谁知这个想法直接将我带上了一段奇妙的创造之旅!
温馨提示:下面信息量可能有点爆炸!!
做电子琴首先要有输入模块,于是我想利用矩阵按键和独立按键来模拟电子琴的键盘,因为矩阵键盘只有16个键,对于36个从低中高还有声调的音符显然不够,于是我就想用独立按键的前三位来控制低中高三中音调,用矩阵按键前12个按键来控制音符,想法很美好但我实践的时候却发现之前写独立按键模块时的代码是利用delay延迟和while循环来给按键消抖并实验放手返回值,但如果同时按下独立按键和矩阵键盘就会发现程序卡死在while循环里导致读出的矩阵按键值可能是上一次的值,所以我思考了一下决定用定时器中断来解决这个问题,通过中断触发来定时比较独立按键的返回值来确定按键是否放开,这样就不会让程序卡死在while循环里了!
#include
/**
* @brief 定时器0初始化,1毫秒@12.000MHz
* @param 无
* @retval 无
*/
void Timer0Init(void)
{
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0=1;
EA=1;
PT0=0;
}
/*定时器中断函数模板
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count;
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++;
if(T0Count>=1000)
{
T0Count=0;
}
}
*/
#include
#include "Delay.h"
unsigned char Key_KeyNumber;
unsigned char Key(void)
{
unsigned char Temp=0;
Temp=Key_KeyNumber;
Key_KeyNumber=0;
return Temp;
}
unsigned char Key_GetState()
{
unsigned char KeyNumber=0;
if(P3_1==0){KeyNumber=1;}
if(P3_0==0){KeyNumber=2;}
if(P3_2==0){KeyNumber=3;}
if(P3_3==0){KeyNumber=4;}
return KeyNumber;
}
void Key_Loop(void)
{
static unsigned char NowState,LastState;
LastState=NowState; //按键状态更新
NowState=Key_GetState(); //获取当前按键状态
//如果上个时间点按键按下,这个时间点未按下,则是松手瞬间,以此避免消抖和松手检测
if(LastState==1 && NowState==0)
{
Key_KeyNumber=1;
}
if(LastState==2 && NowState==0)
{
Key_KeyNumber=2;
}
if(LastState==3 && NowState==0)
{
Key_KeyNumber=3;
}
if(LastState==4 && NowState==0)
{
Key_KeyNumber=4;
}
}
这个是优化后的独立按键模块,只要在主函数的中断函数内调用Key_Loop子函数就可以定时比较来确定按键状态了。
# include
# include "delay.h"
int Matrix()
{
int number;
P1=0xFF;
P1_3=0;
if(P1_7==0){delay(20);while(P1_7==0);delay(20);number=1;}
if(P1_6==0){delay(20);while(P1_6==0);delay(20);number=5;}
if(P1_5==0){delay(20);while(P1_5==0);delay(20);number=9;}
if(P1_4==0){delay(20);while(P1_4==0);delay(20);number=13;}
P1=0xFF;
P1_2=0;
if(P1_7==0){delay(20);while(P1_7==0);delay(20);number=2;}
if(P1_6==0){delay(20);while(P1_6==0);delay(20);number=6;}
if(P1_5==0){delay(20);while(P1_5==0);delay(20);number=10;}
if(P1_4==0){delay(20);while(P1_4==0);delay(20);number=14;}
P1=0xFF;
P1_1=0;
if(P1_7==0){delay(20);while(P1_7==0);delay(20);number=3;}
if(P1_6==0){delay(20);while(P1_6==0);delay(20);number=7;}
if(P1_5==0){delay(20);while(P1_5==0);delay(20);number=11;}
if(P1_4==0){delay(20);while(P1_4==0);delay(20);number=15;}
P1=0xFF;
P1_0=0;
if(P1_7==0){delay(20);while(P1_7==0);delay(20);number=4;}
if(P1_6==0){delay(20);while(P1_6==0);delay(20);number=8;}
if(P1_5==0){delay(20);while(P1_5==0);delay(20);number=12;}
if(P1_4==0){delay(20);while(P1_4==0);delay(20);number=16;}
return number;
}
这里附上矩阵按键模块,我这里没有优化不过不影响最后的效果,想要优化的小伙伴可以参照独立按键的思路来操作。
解决好键盘模拟后还要解决数据读写问题,刚开始我打算在主函数里面定义一个音符索引和节拍索引数组直接将按键值写入数组,但我发现如果将索引写入数组是不够用的,所以我想结合I2C总线和AT24C02存储器将音符索引和节拍索引数组写入其中到播放时再读出,这样就可以扩容了。
# include
sbit I2C_SCL=P2^1;
sbit I2C_SDA=P2^0;
void I2C_Start()
{
I2C_SDA=1;
I2C_SCL=1;
I2C_SDA=0;
I2C_SCL=0;
}
void I2C_Stop()
{
I2C_SDA=0;
I2C_SCL=1;
I2C_SDA=1;
}
void I2C_SendData(unsigned char Data)
{ unsigned char i;
for(i=0;i<8;i++)
{
I2C_SDA=Data&(0x80>>i);
I2C_SCL=1;
I2C_SCL=0;
}
}
unsigned char I2C_ReceiveData()
{
unsigned char i,Data=0x00;
I2C_SDA=1;
for(i=0;i<8;i++)
{
I2C_SCL=1;
if(I2C_SDA){Data|=(0x80>>i);}
I2C_SCL=0;
}
return Data;
}
void I2C_SendAck(unsigned char Ack)
{
I2C_SDA=Ack;
I2C_SCL=1;
I2C_SCL=0;
}
unsigned char I2C_ReceiveAck()
{
unsigned char Ack;
I2C_SDA=1;
I2C_SCL=1;
Ack=I2C_SDA;
I2C_SCL=0;
return Ack;
}
这个是I2C总线模块,里面定义了6个子函数供其他模块调用来完成数据传输。
# include
# include "I2C.h"
#define AT24C02_ADDRESS 0xA0
void AT24C02_Write(unsigned char DataAddress,Data)
{
I2C_Start();
I2C_SendData(AT24C02_ADDRESS);
I2C_ReceiveAck();
I2C_SendData(DataAddress);
I2C_ReceiveAck();
I2C_SendData(Data);
I2C_ReceiveAck();
I2C_Stop();
}
unsigned char AT24C02_Read(unsigned char DataAddress)
{
unsigned char Data;
I2C_Start();
I2C_SendData(AT24C02_ADDRESS);
I2C_ReceiveAck();
I2C_SendData(DataAddress);
I2C_ReceiveAck();
I2C_Start();
I2C_SendData(AT24C02_ADDRESS|0x01);
I2C_ReceiveAck();
Data=I2C_ReceiveData();
I2C_SendAck(1);
I2C_Stop();
return Data;
}
AT24C02模块就相对简洁了,只需要调用I2C总线模块子函数匹配传收的数据帧原理即可传输数据。
搞定完音符和节拍读写后我好像利用LCD液晶屏来显示写入的音符note和写入的节拍beat,废话不多说,直接上主函数。
# include
# include "delay.h"
# include "Timer0.h"
# include "AT24C02.h"
# include "LCD1602.h"
# include "Key.h"
# include "Matrix.h"
sbit Buzzer=P2^5;
#define SPEED 500
#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 code Frequency[]={
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 Musickey,Frequencykey,KeyNum,Data,i=0,j=0,count=0,M_count=0,Button,a;
void main()
{
LCD_Init();
Timer0Init();
while(1)
{
Button=Key();
KeyNum=Matrix();
if(Button==4&&count==0)
{
count=1;
AT24C02_Write(i+1,0);
}
else if(Button==4&&count==1)
{
count=0;
}
if(count==0)
{
if(i%2==0)
{
M_count=1;
}
else
{
M_count=0;
}
if(Button==1)
{
KeyNum=Matrix();
Data=KeyNum;
AT24C02_Write(i,Data);
delay(5);
Data=AT24C02_Read(i);
delay(5);
i++;
if (M_count==1)
{
if (KeyNum==1||KeyNum==3)
{LCD_ShowString(2,1,"Note: L ");a=(i+1)/2;LCD_ShowNum(2,8,(KeyNum+1)/2,1);LCD_ShowNum(2,11,a,2);}
else if(KeyNum==2||KeyNum==4)
{a=(i+1)/2;LCD_ShowString(2,1,"Note: L _");LCD_ShowNum(2,8,KeyNum/2,1);LCD_ShowNum(2,11,a,2);}
else if(KeyNum==5||KeyNum==6)
{a=(i+1)/2;LCD_ShowString(2,1,"Note: L _");LCD_ShowNum(2,8,KeyNum-2,1);LCD_ShowNum(2,11,a,2);}
else if(KeyNum==8||KeyNum==10||KeyNum==12)
{a=(i+1)/2;LCD_ShowString(2,1,"Note: L ");LCD_ShowNum(2,8,(KeyNum+2)/2,1);LCD_ShowNum(2,11,a,2);}
else if(KeyNum==7||KeyNum==9||KeyNum==11)
{a=(i+1)/2;LCD_ShowString(2,1,"Note: L _");LCD_ShowNum(2,8,(KeyNum+1)/2,1);LCD_ShowNum(2,11,a,2);}
}
else if(M_count==0)
{LCD_ShowString(1,1,"Beat:");LCD_ShowNum(1,7,Data,2);LCD_ShowNum(1,11,i/2,2);}
Data=0;
}
if(Button==2)
{
KeyNum=Matrix();
Data=12*M_count+KeyNum;
AT24C02_Write(i,Data);
delay(5);
Data=AT24C02_Read(i);
delay(5);
i++;
if (M_count==1)
{
if (KeyNum==1||KeyNum==3)
{a=(i+1)/2;LCD_ShowString(2,1,"Note: M ");LCD_ShowNum(2,8,(KeyNum+1)/2,1);LCD_ShowNum(2,11,a,2);}
else if(KeyNum==2||KeyNum==4)
{a=(i+1)/2;LCD_ShowString(2,1,"Note: M _");LCD_ShowNum(2,8,KeyNum/2,1);LCD_ShowNum(2,11,a,2);}
else if(KeyNum==5||KeyNum==6)
{a=(i+1)/2;LCD_ShowString(2,1,"Note: M _");LCD_ShowNum(2,8,KeyNum-2,1);LCD_ShowNum(2,11,a,2);}
else if(KeyNum==8||KeyNum==10||KeyNum==12)
{a=(i+1)/2;LCD_ShowString(2,1,"Note: M ");LCD_ShowNum(2,8,(KeyNum+2)/2,1);LCD_ShowNum(2,11,a,2);}
else if(KeyNum==7||KeyNum==9||KeyNum==11)
{a=(i+1)/2;LCD_ShowString(2,1,"Note: M _");LCD_ShowNum(2,8,(KeyNum+1)/2,1);LCD_ShowNum(2,11,a,2);}
}
else if(M_count==0)
{LCD_ShowString(1,1,"Beat:");LCD_ShowNum(1,7,Data,2);LCD_ShowNum(1,11,i/2,2);}
Data=0;
}
if(Button==3)
{
KeyNum=Matrix();
Data=24*M_count+KeyNum;
AT24C02_Write(i,Data);
delay(5);
Data=AT24C02_Read(i);
delay(5);
i++;
if (M_count==1)
{
if (KeyNum==1||KeyNum==3)
{a=(i+1)/2;LCD_ShowString(2,1,"Note: H ");LCD_ShowNum(2,8,(KeyNum+1)/2,1);LCD_ShowNum(2,11,a,2);}
else if(KeyNum==2||KeyNum==4)
{a=(i+1)/2;LCD_ShowString(2,1,"Note: H _");LCD_ShowNum(2,8,KeyNum/2,1);LCD_ShowNum(2,11,a,2);}
else if(KeyNum==5||KeyNum==6)
{a=(i+1)/2;LCD_ShowString(2,1,"Note: H _");LCD_ShowNum(2,8,KeyNum-2,1);LCD_ShowNum(2,11,a,2);}
else if(KeyNum==8||KeyNum==10||KeyNum==12)
{a=(i+1)/2;LCD_ShowString(2,1,"Note: H ");LCD_ShowNum(2,8,(KeyNum+2)/2,1);LCD_ShowNum(2,11,a,2);}
else if(KeyNum==7||KeyNum==9||KeyNum==11)
{a=(i+1)/2;LCD_ShowString(2,1,"Note: H _");LCD_ShowNum(2,8,(KeyNum+1)/2,1);LCD_ShowNum(2,11,a,2);}
}
else if(M_count==0)
{LCD_ShowString(1,1,"Beat:");LCD_ShowNum(1,7,Data,2);LCD_ShowNum(1,11,i/2,2);}
Data=0;
}
}
if (count==1)
{
for (j=0;j<=i+1;j++)
{
Frequencykey=AT24C02_Read(j);
j++;
Musickey=AT24C02_Read(j);
delay(SPEED/4*Musickey);
TR0 = 0;
delay(10);
TR0 = 1;
if(Frequencykey==0)
{
TR0=0;
}
}
}
}
}
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count1;
if(count==0)
{
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count1++;
if(T0Count1>=20)
{
T0Count1=0;
Key_Loop(); //20ms调用一次按键驱动函数
}
}
else if(Frequency[Frequencykey]&&count==1)
{
TL0 = Frequency[Frequencykey]%256; //设置定时初值
TH0 = Frequency[Frequencykey]/256; //设置定时初值
Buzzer=!Buzzer;
}
}
主函数中一共有几大块进行说明:
1.要建立一个音符索引数组,数组中存放了定时器的重装载值来控制中断输出,其中音符L1表示低音DO,音符L1_表示低音升DO,音符M1则表示中音DO,音符H1则表示高音DO;
2.主函数有读写和播放两个主要模块,通过独立按键第四位来进行按键切换;
3.节拍索引我直接读取矩阵键盘的值来确定,正常一小节16拍,所以就算一小节只有一个音符也够用,至于音符演奏时间的计算就定义一个初始的SPEED,通过节拍四则运算确定每个音符的播放时长;
4.LCD液晶屏显示模块也需要通过复杂的逻辑判断来实现既有字母又有数字的混合显示,而且升调比原调多了一个“_”,所以如果想用指针数组实现也相当麻烦;
5.定时器中断函数需要服务于蜂鸣器和独立按键扫描两个模块,为了避免独立按键扫描时蜂鸣器乱叫,我给读写模块和播放模块通过if判断来执行不同的中断。
在做这个模拟电子琴的实验中我也遇到了很多问题,经过不断调试也取得了一些效果,但仍有一些不完善的地方,有兴趣的小伙伴可以一起玩一下并加以改善。
C51单片机——电子琴实验