本次编程实验以IAP15F2K61S2为单片机主控芯片,头文件为STC15F2K60S2.H
。若用于51系列单片机,以reg52.h
为头文件,则读者需将程序中可能涉及的定时器初始化程序和LED亮灭程序和数码管显示程序,根据自身所用单片机原理图和手册进行修改。
DS1302是DALLAS公司推出的涓流充电时钟芯片,内含有两块存储器:日历时钟寄存器和静态RAM存储器(31字节),后者可用于用户自定义编程。DS1302采用SPI三线接口与单片机进行通信,可向用户提供秒分时日月年的信息,且可通过 AM/PM 指示决定采用 24 或 12 小时格式,每月的天数和闰年的天数可自动调整。
Vcc1和Vcc2:电源供电管脚。其中Vcc1作为主电源,Vcc2作为备用电源。当Vcc2>Vcc1+0.2V时,由Vcc2向DS1302供电;当Vcc2< Vcc1时,由Vcc1向DS1302供电。
X1和X2:32.768KHz 晶振管脚
SCLK:串行时钟输入。控制数据的输入和输出。
I/O: 数据输入和输出引脚
CE:使能信号,在读、写数据期间,必须为高电平。
由原理图可知,SCLK引脚为P1^ 7 ; I /O引脚为P2^ 3;CE引脚为P1^ 3。
时钟日历寄存器包含在 7 个读/写寄存器内,相关数据以BCD码显示存放。BCD码是二进制十进制代码,是一种二进制的数字编码形式,利用四个位元来储存一个十进制的数码,可以快速转换。如10对应的BCD码为0x10,25对应0x25,简单来说就是在十进制前加0x(十六进制标志)即可。时钟日历寄存器从第一行至第七行,每一行分别为秒分时日月星期年寄存器。第八行为写保护寄存器。此外,第一列为读控制字节,第二列为写控制字节(后文介绍)。
7 个读/写寄存器的每一位,并不都是用于存放数据:
1.秒寄存器的 BIT7 定义为时间暂停位,当 BIT1 为 1 时,时钟振荡器停止工作,DS1302 进入低功耗模式,电源消耗小于 100 微安;当 BIT1 为 0 时,时钟振荡器启动,DS1302 正常工作。
2.小时寄存器的 BIT7 定义为 12 或 24 小时工作模式选择位,当 BIT7 为高时,为 12 小时工作模式,此时 BIT5 为 AM/PM 位,低电平标示 AM,高电平标示PM,在 24 小时模式下,BIT5 为第二个 10 小时位标示(20~23 时)。
在对上述寄存器进行数据写入时,必须先将写保护寄存器的 BIT7(即WP,写保护位)置低电平,其他位都置为0;否则不能对任何时钟日历寄存器进行写操作,但可以进行读操作。
在单片机与DS1302进行数据通信时,需要发送一个控制字节,控制字节的意义在于:让DS1302知道在何处如何对数据进行处理(存放或读取)。
位7:必须是逻辑1,如果它为0,则不能把数据写入到DS1302中。
位6:如果为0,则表示存取日历时钟数据,为1表示存取RAM数据。
位5-位1:指示操作单元的地址。
位0(最低有效位):为0,表示要进行写操作,写入数据;为1,表示进行读操作,读出数据。
以对秒寄存器进行写操作为例,控制字节为1000 0000,即0x80,符合时钟日历寄存器中秒寄存器的写控制字节。
写时序:首先CE置高电平,开始的 8 个 SCLK 周期,每迎来一次上升沿,输入写控制字节的一位;数据字节在后 8 个 SCLK 周期的
上升沿输入;最后CE置低电平,结束通信。写控制字节输入和数据字节输入均从位 0 开始。
读时序:首先CE置高电平,开始的 8 个 SCLK 周期,每迎来一次上升沿,输入读控制字节的一位;数据字节在后 8 个 SCLK 周期的
下降沿输出;最后CE置低电平,结束通信。读控制字节输入和数据字节输出均从位 0 开始。需要注意的是:在读控制字节位7输入后,SCLK将迎来一次下降沿,数据字节将从此次下降沿开始发送。
先向DS1302写入数据,设定起始时间:首先关闭写保护,然后写入写控制字节及数据字节,最后打开写保护。向DS1302读取数据时:直接写入读控制字节,即可读出数据。
独立按键短按/长按原理见定时器扫描按键(短按/长按)。
ds1302.h
#ifndef __DS1302_H
#define __DS1302_H
#include
#include
//对DS1302引脚进行I/O口定义
sbit SCK=P1^7; //串行时钟线
sbit SDA=P2^3; //数据线
sbit RST = P1^3; // DS1302使能线
extern unsigned char read_time[]; //声明外部变量
void DS1302_Write(unsigned char temp); //向DS1302写入一个字节
void DS1302_Write_Byte( unsigned char address,unsigned char dat ); //向指定寄存器写入数据
unsigned char DS1302_Read_Byte( unsigned char address ); //从指定寄存器读出数据
void DS1302_set(unsigned char DS1302_set_addr[],unsigned char ds1302_set_time[]); //DS1302初始时间设定
void DS1302_read(unsigned char DS1302_set_addr[]); //DS1302时间读取
#endif
ds1302.c
#include "ds1302.h"
unsigned char read_time[7]={0}; //DS1302时间存储数组
/*
* @brief DS1302通信初始化
* @param
* @reval
* @note:
*/
void DS1302_Start()
{
RST=0; _nop_(); //DS1302复位
SCK=0; _nop_(); //拉低串行时钟线
RST=1; _nop_(); //禁止复位
}
/*
* @brief 向DS1302写入一个字节
* @param byte:要写入的字节
* @reval
* @note:
*/
void DS1302_Write(unsigned char byte)
{
unsigned char i=0;
for (i=0;i<8;i++)
{
SCK=0; //拉低时钟线
SDA=byte&0x01; //取出写入字节的位0,放到数据线上
byte>>=1; //byte字节右移1位,从而位1变成位0,位2变成位1...从而低位先写入
SCK=1; //拉高时钟线,产生上升沿,发送一位数据
}
}
/*
* @brief 向DS1302寄存器写入数据
* @param address:要写入数据的寄存器;dat:要写入的数据
* @reval
* @note:
*/
void DS1302_Write_Byte( unsigned char address,unsigned char dat )
{
DS1302_Start(); //DS1302通信初始化
DS1302_Write(address); //写入控制字节,即指明寄存器和读/写操作
DS1302_Write(dat); //写入数据
RST=0; //通信结束
}
/*
* @brief 从DS1302寄存器读取数据
* @param address:待读取数据的寄存器
* @reval temp:读出的数据
* @note:
*/
unsigned char DS1302_Read_Byte ( unsigned char address )
{
unsigned char i=0,temp=0x00;
DS1302_Start(); //DS1302通信初始化
DS1302_Write(address); //写入控制字节,即指明寄存器和读/写操作,此行代码运行结束后,SCK=1
for (i=0;i<8;i++)
{
SCK=0; //拉低时钟线,产生下降沿,从而开始发送一位数据
temp>>=1; //temp右移一位,清零位7
if(SDA) //与下一行代码搭配,如果SDA=1,则temp位7为1;反之为0,从而读出数据。
temp|=0x80; //读出一位,并赋值给temp的位7,下次循环时,temp右移一位,使得位7变成位6,位6变成位5...从而低位先读出
SCK=1; //拉高时钟线
}
RST=0; _nop_(); //结束通信
SCK=0; _nop_(); //拉低时钟线
SCK=1; _nop_(); //拉高时钟线,实质是复位到高电平
SDA=0; _nop_();
SDA=1; _nop_(); //复位数据线到高电平
return (temp); //返回读出的数据字节
}
/*
* @brief DS1302初始时间设定
* @param DS1302_set_addr[]:时钟日历7个写寄存器地址;DS1302_set_time[]:预设的时间(BCD码)
* @reval
* @note:
*/
void DS1302_set(unsigned char DS1302_set_addr[],unsigned char DS1302_set_time[])
{
unsigned char i=0;
DS1302_Write_Byte(0x8e,0x00); //关闭写保护
for(i=0;i<7;i++)
{
DS1302_Write_Byte(DS1302_set_addr[i],DS1302_set_time[i]);//向时钟日历寄存器写入相应数据
}
DS1302_Write_Byte(0x8e,0x80); //打开写保护
}
/*
* @brief DS1302时间读取
* @param DS1302_set_addr[]:时钟日历7个写寄存器地址
* @reval
* @note:
*/
void DS1302_read(unsigned char DS1302_set_addr[])
{
unsigned char i=0;
for(i=0;i<7;i++)
{
read_time[i]=DS1302_Read_Byte(DS1302_set_addr[i]+0x01);//注意到每个时钟日历读寄存器地址比写寄存器地址大1,所以读寄存器地址在写寄存器地址基础上加1
}
}
key.h
#ifndef __KEY_H
#define __KEY_H
#include
sbit key1=P3^0; //按键连接的IO口定义
sbit key2=P3^1;
sbit key3=P3^2;
sbit key4=P3^3;
unsigned char key_state(); //按键状态获取函数
unsigned char key_send(unsigned char mode); //按键值发送函数
#endif
key.c
#include "key.h"
unsigned char key_state() //得到按键状态
{
unsigned char keyget=0;
if(!key1)keyget=1;
if(!key2)keyget=2;
if(!key3)keyget=3;
if(!key4)keyget=4;
return keyget;
}
/*
* @brief 按键值发送函数
* @param mode:按键长短按模式标志,1长按0短按
* @reval keyend:最终返回的按键值
* @note
*/
unsigned char key_send(unsigned char mode)
{
static unsigned char keynow=0,keylast=0; //按键当前状态;按键上一次状态
unsigned char keyend=0; //最终要发送的按键值
keylast=keynow; //不断刷新按键上一次状态和当前状态
keynow=key_state();
if(keylast==1&&keynow==(1*mode)) //如果按键1按下
keyend=1;
if(keylast==2&&keynow==(2*mode)) //如果按键2按下
keyend=2;
if(keylast==3&&keynow==(3*mode)) //如果按键3按下
keyend=3;
if(keylast==4&&keynow==(4*mode)) //如果按键4按下
keyend=4;
return keyend;
}
timer0.h
#ifndef __TIMER0_H
#define __TIMER0_H
#include
void Timer0_Init(void); //1毫秒@11.0592MHz
#endif
timer0.c
#include "timer0.h"
void Timer0_Init(void) //1毫秒@11.0592MHz
{
AUXR &= 0x7F; //定时器时钟12T模式
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x66; //设置定时初始值
TH0 = 0xFC; //设置定时初始值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0 = 1; //使能定时器0中断
EA=1;
}
main.c
#include
#include "timer0.h"
#include "key.h"
#include "ds1302.h"
#define outputp0(y,x) P0=x,P2&=0x1f,P2=y,P2&=0x1f;
typedef unsigned char u8;
u8 code DS1302_write_addr[]={0x80,0x82,0x84,0x86,0x88,0x8a,0x8c}; //时钟日历7个写寄存器地址
u8 DS1302_write_time[]={0x45,0x59,0x23,0x31,0x12,0x07,0x99}; //预设的时间(BCD码)
u8 DS1302_write_time_temp[]={45,59,23,31,12,07,9}; //预设时间的中间缓存变量(十进制)
u8 keynum=0,keytemp=0,setflag=0,showflag=0;
unsigned char code Seg_Table[]={ //共阳数码管
0xc0,
0xf9,
0xa4,
0xb0,
0x99,
0x92,
0x82,
0xf8,
0x80,
0x90,
0xff,
0xbf //'-'
};
void Delay2ms(void) //@11.0592MHz
{
unsigned char data i, j;
_nop_();
_nop_();
i = 22;
j = 128;
do
{
while (--j);
} while (--i);
}
/*
* @brief 1位数码管显示函数
* @param pos:位选;dat:显示数字;dot:小数点选择位,1有0无
* @reval
* @note:
*/
void showbit(unsigned char pos,dat,dot)
{
outputp0(0xc0,0x01<<pos); //位选
outputp0(0xe0,Seg_Table[dat]+0x80*dot); //段选
Delay2ms(); //延时
outputp0(0xc0,0x01<<pos); //位选
outputp0(0xe0,Seg_Table[10]); //段选,这两行用于消影
}
void test() //测试程序
{
ET0 = 0; //关闭定时器0中断
DS1302_read(DS1302_write_addr); //读出DS1302时间
ET0 = 1; //打开定时器0中断
if(keynum==4) //如果key4按下
{
showflag=!showflag; //切换显示页面和设置模式
keynum=0; //清零按键值
}
if(keynum<4&&keynum>0) //如果key1/2/3按下
{
u8 i=0;
ET0 = 0; //关闭定时器0中断
DS1302_read(DS1302_write_addr); //读出DS1302当前时间,并赋值给写入时间缓存变量,使时间更改在当前时间基础上进行
for(i=0;i<7;i++)
DS1302_write_time_temp[i]=read_time[i]/16*10+read_time[i]%16; //二-十六进制转二-十进制,方便后续处理
if(showflag==0) //如果是秒分时设置模式
{
switch(keynum) //如果直接对DS1302_write_time[]中的数据加一,则0x19加一变成0x1A,但DS1302数据以BCD码形式存放,不存在“0xA”
{
case 1:DS1302_write_time_temp[0]++;break; //key1按下使秒加一
case 2:DS1302_write_time_temp[1]++;break; //key2按下使分加一
case 3:DS1302_write_time_temp[2]++;break; //key3按下使时加一
}
DS1302_write_time_temp[0]%=60; //范围限定
DS1302_write_time_temp[1]%=60;
DS1302_write_time_temp[2]%=24;
DS1302_write_time[0]=DS1302_write_time_temp[0]/10*16+DS1302_write_time_temp[0]%10; //二-十进制转二-十六进制,方便写入时间到DS1302
DS1302_write_time[1]=DS1302_write_time_temp[1]/10*16+DS1302_write_time_temp[1]%10;
DS1302_write_time[2]=DS1302_write_time_temp[2]/10*16+DS1302_write_time_temp[2]%10;
}
else if(showflag==1) //如果是年月日设置模式
{
switch(keynum)
{
case 1:DS1302_write_time_temp[3]++;break; //key1按下使日加一
case 2:DS1302_write_time_temp[4]++;break; //key2按下使月加一
case 3:DS1302_write_time_temp[6]++;break; //key3按下使年加一
}
if(DS1302_write_time_temp[4]>12)DS1302_write_time_temp[4]=1; //月范围限制
DS1302_write_time_temp[6]%=100; //年范围限制,只显示年份后两位(00-99)
//下面是根据年份和月对日范围进行限制,请读者举一反三,自行思考代码含义
if(DS1302_write_time_temp[4]<8)
{
if(DS1302_write_time_temp[4]%2==0)
DS1302_write_time_temp[3]%=31;
else if(DS1302_write_time_temp[4]%2==1)
DS1302_write_time_temp[3]%=32;
}
else if(DS1302_write_time_temp[4]==8)
DS1302_write_time_temp[3]%=32;
else if(DS1302_write_time_temp[4]>8)
{
if(DS1302_write_time_temp[4]%2==0)
DS1302_write_time_temp[3]%=32;
else if(DS1302_write_time_temp[4]%2==1)
DS1302_write_time_temp[3]%=31;
}
if(DS1302_write_time_temp[4]==2)
{
if(((2000+DS1302_write_time_temp[6])%4)==0)
DS1302_write_time_temp[3]%=29;
else if(((2000+DS1302_write_time_temp[6])%4)!=0)
DS1302_write_time_temp[3]%=30;
}
if(DS1302_write_time_temp[3]==0)DS1302_write_time_temp[3]=1;
DS1302_write_time[3]=DS1302_write_time_temp[3]/10*16+DS1302_write_time_temp[3]%10; //二-十进制转二-十六进制,方便写入时间到DS1302
DS1302_write_time[4]=DS1302_write_time_temp[4]/10*16+DS1302_write_time_temp[4]%10;
DS1302_write_time[6]=DS1302_write_time_temp[6]/10*16+DS1302_write_time_temp[6]%10;
}
DS1302_set(DS1302_write_addr,DS1302_write_time); //预设DS1302时间
DS1302_read(DS1302_write_addr); //读出DS1302时间
keynum=0; //清零按键值
ET0 = 1; //打开定时器0中断
}
if(showflag==0) //显示秒分时
{
showbit(0,read_time[2]/16,0);
showbit(1,read_time[2]%16,0);
showbit(2,11,0);
showbit(3,read_time[1]/16,0);
showbit(4,read_time[1]%16,0);
showbit(5,11,0);
showbit(6,read_time[0]/16,0);
showbit(7,read_time[0]%16,0);
}
else if(showflag==1) //显示年月日
{
showbit(0,read_time[6]/16,0);
showbit(1,read_time[6]%16,0);
showbit(2,11,0);
showbit(3,read_time[4]/16,0);
showbit(4,read_time[4]%16,0);
showbit(5,11,0);
showbit(6,read_time[3]/16,0);
showbit(7,read_time[3]%16,0);
}
}
void main()
{
outputp0(0x80,0xff); //关闭8位LED
outputp0(0xa0,0x00); //关闭蜂鸣器继电器等
Timer0_Init(); //定时器0初始化
ET0 = 0; //关闭定时器0中断
DS1302_set(DS1302_write_addr,DS1302_write_time); //预设DS1302时间
ET0 = 1; //打开定时器0中断
while(1)
{
test();
}
}
void Timer0_Isr(void) interrupt 1
{
static u8 longflag=0;
static int tkey=0,tlongflag=0,tlongkey=0;
TL0 = 0x66; //设置定时初始值
TH0 = 0xFC; //设置定时初始值
tkey++; //按键短按时间计数
if(tkey>=(500*longflag+25*(!longflag))) //短按每25ms检测一次按键,长按时防止松手瞬间触发短按
{
if(!(key1&key2&key3&key4)) //长按检测,每25ms检测按键是否仍在按下,如果是
{
tlongflag++; //按键按下时间计数(以25ms为单位)
if(tlongflag>=80) //如果持续按下2s
{
tlongflag=80; //防止溢出
longflag=1; //切换为长按模式
}
}
else if(key1&key2&key3&key4) //长按检测,每25ms检测按键是否仍在按下,如果不是
{
tlongflag=0; //清零按键按下时间计数
longflag=0; //切换为短按模式
}
tkey=0; //清零短按时间计数
if(longflag==0) //如果是短按模式
{
keytemp=key_send(0); //按键以短按模式检测
if(keytemp)
{
keynum=keytemp;
setflag=1;
}
}
}
tlongkey++; //按键长按时间计数
if(tlongkey>=500) //长按时每500ms检测一次按键
{
tlongkey=0; //清零按键长按时间计数
if(longflag==1) //如果是长按模式
{
keytemp=key_send(1); //按键以长按模式检测
if(keytemp)
{
keynum=keytemp;
setflag=1;
}
}
}
}
上电显示“23-59-45”,正常读取时间;按下key1/2/3,相应数据改变;上电后15秒内按下key4,切换显示,显示“99-12-31”。若在上电后15秒后切换到年月日显示,由于初始设定时间为“99-12-31-23-59-45”,相当于过去了一天,因此年月日显示为“00-01-01”。需要注意的是:设置时间时,DS1302停止工作,即秒停止自增;显示年月日时,并不妨碍秒分时的改变,DS1302正常工作。
此次编程实验实现了定时器扫描按键和具有严格时序通信(此次实验为SPI)并存。方法为在通信前关闭中断,在通信完成后打开中断。根据此种方法,可以在通过中断提高MCU工作效率的同时,使中断不干扰具有严格时序的通信(如I2C,One-Wire,SPI等)。
有任何问题和补充,欢迎私信或评论区交流。