效果视频:【内鬼时钟】基于stm32f103的实时时钟_哔哩哔哩_bilibili
项目链接:https://github.com/endless91595/stm32-rtc
一,硬件准备
① 正点原子stm32f103 mini板
② alientek 正点原子 tftlcd 2.8寸
③ 有源蜂鸣器
④ 杜邦线
二,项目功能
① 有基本计时功能,精确到秒
② 有基本的闹钟功能
③ 有整点报时功能
④ 可使用电脑串口调节时间及设置闹钟
⑤ 有良好的人机交互功能及用户体验
三,源码简解
一,RTC实时时钟部分
本劣作RTC部分采用了正点原子的RTC例程
RTC的配置步骤:
Ⅰ 使能电源时钟和备份区域时钟
Ⅱ 取消备份区写保护
Ⅲ 复位备份区域,开启外部低俗振荡器
Ⅳ 选择并使能rtc时钟
Ⅵ 设置rtc时钟分频,配置rtc时钟
Ⅶ 更新配置,设置rtc中断分组,编写rtc中断服务函数
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "rtc.h"
#include "BEEP.h"
#include "LED.h"
_calendar_obj calendar;//时钟结构体
_calendar_obj fade_calendar;
_calendar_obj ALA_calendar;
static void RTC_NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn; //RTC全局中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //先占优先级1位,从优先级3位
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //先占优先级0位,从优先级4位
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能该通道中断
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
}
//实时时钟配置
//初始化RTC时钟,同时检测时钟是否工作正常
//BKP->DR1用于保存是否第一次配置的设置
//返回0:正常
//其他:错误代码
u8 RTC_Init(void)
{
//检查是不是第一次配置时钟
u8 temp=0;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); //使能PWR和BKP外设时钟
PWR_BackupAccessCmd(ENABLE); //使能后备寄存器访问
if (BKP_ReadBackupRegister(BKP_DR1) != 0x5051) //从指定的后备寄存器中读出数据:读出了与写入的指定数据不相乎
{
BKP_DeInit(); //复位备份区域
RCC_LSEConfig(RCC_LSE_ON); //设置外部低速晶振(LSE),使用外设低速晶振
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) //检查指定的RCC标志位设置与否,等待低速晶振就绪
{
temp++;
delay_ms(10);
}
if(temp>=250)return 1;//初始化时钟失败,晶振有问题
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //设置RTC时钟(RTCCLK),选择LSE作为RTC时钟
RCC_RTCCLKCmd(ENABLE); //使能RTC时钟
RTC_WaitForLastTask(); //等待最近一次对RTC寄存器的写操作完成
RTC_WaitForSynchro(); //等待RTC寄存器同步
RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能RTC秒中断
RTC_ITConfig(RTC_IT_ALR, ENABLE);
RTC_WaitForLastTask(); //等待最近一次对RTC寄存器的写操作完成
RTC_EnterConfigMode();/// 允许配置
RTC_SetPrescaler(32767); //设置RTC预分频的值
RTC_WaitForLastTask(); //等待最近一次对RTC寄存器的写操作完成
RTC_Set(2022,3,2,18,15,12); //设置时间
RTC_ExitConfigMode(); //退出配置模式
BKP_WriteBackupRegister(BKP_DR1, 0X5051); //向指定的后备寄存器中写入用户程序数据
}
else//系统继续计时
{
RTC_WaitForSynchro(); //等待最近一次对RTC寄存器的写操作完成
RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能RTC秒中断
RTC_WaitForLastTask(); //等待最近一次对RTC寄存器的写操作完成
}
RTC_NVIC_Config();//RCT中断分组设置
RTC_Get();//更新时间
return 0; //ok
}
//RTC时钟中断
//每秒触发一次
//extern u16 tcnt;
void RTC_IRQHandler(void)
{
if (RTC_GetITStatus(RTC_IT_SEC) != RESET)//秒钟中断
{
fade_calendar = calendar;
RTC_Get();//更新时间
}
if(RTC_GetITStatus(RTC_IT_ALR)!= RESET)//闹钟中断
{
RTC_ClearITPendingBit(RTC_IT_ALR); //清闹钟中断
}
RTC_ClearITPendingBit(RTC_IT_SEC|RTC_IT_OW); //清闹钟中断
RTC_WaitForLastTask();
}
//判断是否是闰年函数
//月份 1 2 3 4 5 6 7 8 9 10 11 12
//闰年 31 29 31 30 31 30 31 31 30 31 30 31
//非闰年 31 28 31 30 31 30 31 31 30 31 30 31
//输入:年份
//输出:该年份是不是闰年.1,是.0,不是
u8 Is_Leap_Year(u16 year)
{
if(year%4==0) //必须能被4整除
{
if(year%100==0)
{
if(year%400==0)return 1;//如果以00结尾,还要能被400整除
else return 0;
}else return 1;
}else return 0;
}
//设置时钟
//把输入的时钟转换为秒钟
//以1970年1月1日为基准
//1970~2099年为合法年份
//返回值:0,成功;其他:错误代码.
//月份数据表
u8 const table_week[12]={0,3,3,6,1,4,6,2,5,0,3,5}; //月修正数据表
//平年的月份日期表
const u8 mon_table[12]={31,28,31,30,31,30,31,31,30,31,30,31};
u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec)
{
u16 t;
u32 seccount=0;
if(syear<1970||syear>2099)return 1;
for(t=1970;t<syear;t++) //把所有年份的秒钟相加
{
if(Is_Leap_Year(t))seccount+=31622400;//闰年的秒钟数
else seccount+=31536000; //平年的秒钟数
}
smon-=1;
for(t=0;t<smon;t++) //把前面月份的秒钟数相加
{
seccount+=(u32)mon_table[t]*86400;//月份秒钟数相加
if(Is_Leap_Year(syear)&&t==1)seccount+=86400;//闰年2月份增加一天的秒钟数
}
seccount+=(u32)(sday-1)*86400;//把前面日期的秒钟数相加
seccount+=(u32)hour*3600;//小时秒钟数
seccount+=(u32)min*60; //分钟秒钟数
seccount+=sec;//最后的秒钟加上去
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); //使能PWR和BKP外设时钟
PWR_BackupAccessCmd(ENABLE); //使能RTC和后备寄存器访问
RTC_SetCounter(seccount); //设置RTC计数器的值
RTC_WaitForLastTask(); //等待最近一次对RTC寄存器的写操作完成
RTC_Get();
return 0;
}
//得到当前的时间
//返回值:0,成功;其他:错误代码.
u8 RTC_Alarm_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec)
{
u16 t;
u32 seccount=0;
ALA_calendar.w_year= syear;
ALA_calendar.w_month= smon;
ALA_calendar.w_date= sday;
ALA_calendar.hour= hour;
ALA_calendar.min= min;
ALA_calendar.sec= sec;
ALA_calendar.week = calendar.week;
if(syear<1970||syear>2099)return 1;
for(t=1970;t<syear;t++) //把所有年份的秒钟相加
{
if(Is_Leap_Year(t))seccount+=31622400;//闰年的秒钟数
else seccount+=31536000; //平年的秒钟数
}
smon-=1;
for(t=0;t<smon;t++) //把前面月份的秒钟数相加
{
seccount+=(u32)mon_table[t]*86400;//月份秒钟数相加
if(Is_Leap_Year(syear)&&t==1)seccount+=86400;//闰年2月份增加一天的秒钟数
}
seccount+=(u32)(sday-1)*86400;//把前面日期的秒钟数相加
seccount+=(u32)hour*3600;//小时秒钟数
seccount+=(u32)min*60; //分钟秒钟数
seccount+=sec;//最后的秒钟加上去
//设置时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); //使能PWR和BKP外设时钟
PWR_BackupAccessCmd(ENABLE); //使能后备寄存器访问
//上面三步是必须的!
RTC_SetAlarm(seccount);
RTC_WaitForLastTask(); //等待最近一次对RTC寄存器的写操作完成
return 0;
}
u8 RTC_Get(void)
{
static u16 daycnt=0;
u32 timecount=0;
u32 temp=0;
u16 temp1=0;
timecount=RTC_GetCounter();
temp=timecount/86400; //得到天数(秒钟数对应的)
if(daycnt!=temp)//超过一天了
{
daycnt=temp;
temp1=1970; //从1970年开始
while(temp>=365)
{
if(Is_Leap_Year(temp1))//是闰年
{
if(temp>=366)temp-=366;//闰年的秒钟数
else {temp1++;break;}
}
else temp-=365; //平年
temp1++;
}
calendar.w_year=temp1;//得到年份
temp1=0;
while(temp>=28)//超过了一个月
{
if(Is_Leap_Year(calendar.w_year)&&temp1==1)//当年是不是闰年/2月份
{
if(temp>=29)temp-=29;//闰年的秒钟数
else break;
}
else
{
if(temp>=mon_table[temp1])temp-=mon_table[temp1];//平年
else break;
}
temp1++;
}
calendar.w_month=temp1+1; //得到月份
calendar.w_date=temp+1; //得到日期
}
temp=timecount%86400; //得到秒钟数
calendar.hour=temp/3600; //小时
calendar.min=(temp%3600)/60; //分钟
calendar.sec=(temp%3600)%60; //秒钟
calendar.week=RTC_Get_Week(calendar.w_year,calendar.w_month,calendar.w_date);//获取星期
return 0;
}
//获得现在是星期几
//功能描述:输入公历日期得到星期(只允许1901-2099年)
//输入参数:公历年月日
//返回值:星期号
u8 RTC_Get_Week(u16 year,u8 month,u8 day)
{
u16 temp2;
u8 yearH,yearL;
yearH=year/100; yearL=year%100;
// 如果为21世纪,年份数加100
if (yearH>19)yearL+=100;
// 所过闰年数只算1900年之后的
temp2=yearL+yearL/4;
temp2=temp2%7;
temp2=temp2+day+table_week[month-1];
if (yearL%4==0&&month<3)temp2--;
return(temp2%7);
}
我在秒中断这里令fade_calendar等于原来的calendar结构体 再在main函数里面用判断fade_calendar与calendar的成员变量的异同,相同则pass,不同则刷新屏幕(刷新机制在下文)
二,LCD部分
一,贴图
步骤一:先用Photoshop改变图像大小,确保图片在lcd上满屏显示
步骤二:使用image2lcd软件对图片取模再将取模的文件放入工程内
图为image2lcd的设置
二,数字显示部分
本劣作使用了封装的字体库
显示效果为数码管字体,只用根据入口函数设置坐标 字体形状 数字
即可
#ifndef __DIGITAL_LED_H
#define __DIGITAL_LED_H
#ifndef uint8_t
#define uint8_t unsigned char
#endif
#ifndef uint16_t
#define uint16_t unsigned short
#endif
#ifndef uint32_t
#define uint32_t unsigned int
#endif
typedef struct
{
uint16_t Color_Front;
uint16_t Color_Front_Emp;
uint16_t Color_Back;
uint16_t Draw_Pen_Size;
uint8_t Auto_Size_Scal;
uint8_t (*IF_DrawLine)(uint16_t xs,uint16_t ys,uint16_t xe,uint16_t ye); //IO初始化函数
void (*Digital_Draw_Point)(uint16_t xs,uint16_t ys,uint16_t color);
void (*Digital_Draw_Line)(uint16_t xs,uint16_t ys,uint16_t xe,uint16_t ye,uint16_t color);
} _Draw_Str;
extern uint16_t Color_Point;
extern _Draw_Str draw_str;
#define PARA_DRAW_EXTE (0x01<<6) //显示未点亮部分白色
#define PARA_FILL_MAIN (0x01<<7) //填充数码管
#define PARA_AUTO_SIZE (0x01<<5) //自动控制数码管间隙
#define PARA_OVER_DRAW (0x01<<4) //强制覆盖背景
#define PARA_SHOW_POINT (0x01<<3) //显示小数点
#define PARA_NUM_TYPE (0x01<<2) //类型为原始段码
void Draw_Digital_num(uint16_t x,uint16_t y,uint8_t lw,uint16_t w,uint16_t h,uint8_t num,uint8_t fill);
/**
起始坐标
(x,y)___________________________________________
/ A \ <-------------横线的宽度lw 值越小数码管越细长 ^
/\___________________________________________/\ |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| F| | | |
| | | B| |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| |_________________________________________| | |
\ / \ / |
/ \____________________G____________________/ \ 数码管高度h
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| E| | C| |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| |_________________________________________| | |
\/ \/ |
\_____________________D_____________________/ |
<---------------数码管宽度w--------------------->
例如显示数字1,则B,C段为显示的有效段,其余为无效段,mode参数的位5为1时则无效段是显示的不然就是显示成背景色和不显示一样,
如果另其显示的同时位6也为1,那么显示的无效段就是填充成实心的,不然就是空心的只有边框
主显示段也就是有效段是一直显示的,可以通过位7设置有效的这段是空心还是实心,为1是实心,否则空心
*起始坐标XY,数码管段线宽,显示大小宽高,显示的数字
*mode :bit7 是否填充主显示段,bit6 是否填充副显示段,bit5 是否显示副显示段
*/
void Digital_Draw_num(uint16_t x,uint16_t y,uint8_t lw,uint16_t w,uint16_t h,uint8_t num,uint8_t mode);
#endif
根据字体库,对年月日时分秒分开写显示的函数 (如果把全部写在一个函数则每秒都刷新效果则不好)
#include "shownum.h"
void DrawLine(u16 x1, u16 y1, u16 x2, u16 y2,u16 color)
{
POINT_COLOR = NOKIA;
LCD_DrawLine(x1,y1,x2,y2);
}
void DrawLine2(u16 x1, u16 y1, u16 x2, u16 y2,u16 color)
{
POINT_COLOR = NOKIA;
LCD_DrawLine(x1,y1,x2,y2);
}
void DrawLine3_CLEAR(u16 x1, u16 y1, u16 x2, u16 y2,u16 color)
{
POINT_COLOR = BACKGROUND;
LCD_DrawLine(x1,y1,x2,y2);
}
void Drawday(u16 year,u16 month,u16 date)
{
u16 temp1,temp2,temp3,temp4,temp5,temp6,temp;
u16 tempa,tempb,tempc;
u16 tempa1,tempb1,tempc1;
temp = year;
temp4 = temp%10;
temp3 = temp/10%10;
temp2 = temp/10/10%10;
temp1 = temp/10/10/10&10;
tempa = month;
tempb = tempa%10;
tempc = tempa/10;
tempa1= date;
tempb1 = tempa1%10;
tempc1 = tempa1/10;
Digital_Draw_num(78,38,1,12,16,temp1,0);
Digital_Draw_num(95,38,1,12,16,temp2,0);
Digital_Draw_num(112,38,1,12,16,temp3,0);
Digital_Draw_num(129,38,1,12,16,temp4,0);
Digital_Draw_num(149,38,1,12,16,tempc,0);
Digital_Draw_num(166,38,1,12,16,tempa,0);
Digital_Draw_num(186,38,1,12,16,tempc1,0);
Digital_Draw_num(203,38,1,12,16,tempb1,0);
}
void Drawhour(u16 hour)
{
u16 temp1,temp2,temp3,temp4,temp5;
temp1 = hour;
temp2 = hour/10;
temp3 = hour%10;
Digital_Draw_num(90,63,6,25,40,temp2,0);
Digital_Draw_num(120,63,6,25,40,temp3,0);
}
void Drawmin(u16 min)
{
u16 temp1,temp2,temp3;
temp1 = min;
temp2 = min/10;
temp3 = min%10;
Digital_Draw_num(160,63,6,25,40,temp2,0);
Digital_Draw_num(190,63,6,25,40,temp3,0);
}
void Drawsec1(u16 sec)
{
u16 temp1;
temp1 = sec/10;
Digital_Draw_num(130,111,4,40,36,temp1,1);
}
void Drawsec2(u16 sec)
{
u16 temp2;
temp2 = sec%10;
Digital_Draw_num(180,111,4,40,36,temp2,1);
}
如上文提到,先进行判断,如果fade_calendar的某个成员变量与calendar的成员变量不同则进行lcd刷新,注意fade_calendar实质就是上一秒钟的calendar,刷新的步骤如下
① 把要刷新的部位(年月日或者时分秒)用背景色重新显示一遍
② 再用字体的颜色把新的部位重新打印到lcd上
部分代码如下
while(1)
{
if(fade_calendar.w_year!=calendar.w_year||fade_calendar.w_date!=calendar.w_date)
{
draw_str.Digital_Draw_Line =DrawLine3_CLEAR;
Drawday(fade_calendar.w_year,fade_calendar.w_month,fade_calendar.w_date);
draw_str.Digital_Draw_Line =DrawLine;
Drawday(calendar.w_year,calendar.w_month,calendar.w_date);
}
if(fade_calendar.hour!=calendar.hour)
{
draw_str.Digital_Draw_Line =DrawLine3_CLEAR;
Drawhour(fade_calendar.hour);
draw_str.Digital_Draw_Line =DrawLine;
Drawhour(calendar.hour);
}
if(fade_calendar.min!=calendar.min)
{
draw_str.Digital_Draw_Line =DrawLine3_CLEAR;
Drawmin(fade_calendar.min);
draw_str.Digital_Draw_Line =DrawLine;
Drawmin(calendar.min);
}
if(fade_calendar.sec!=calendar.sec)
{
if(fade_calendar.sec/10!=calendar.sec/10)
{
draw_str.Digital_Draw_Line =DrawLine3_CLEAR;
Drawsec1(fade_calendar.sec);
draw_str.Digital_Draw_Line =DrawLine2;
Drawsec1(calendar.sec);
}
if(calendar.min==0 && zd==1)
{
BEEP = 0;
delay_ms(388);
BEEP = 1;
delay_ms(100);
BEEP = 0;
delay_ms(388);
BEEP = 1;
zd=0;
}else if(calendar.min==0 && zd==0)
{
delay_ms(800);
}
else{
delay_ms(800);
zd = 1;
}
if(fade_calendar.sec%10!=calendar.sec%10)
{
draw_str.Digital_Draw_Line =DrawLine3_CLEAR;
Drawsec2(fade_calendar.sec);
draw_str.Digital_Draw_Line =DrawLine2;
Drawsec2(calendar.sec);
}
}
三,整点报时部分
思路:在上文循环的秒 判断中加入 一个分钟判断,如果分钟等于0,则出发整点报时,同时还要设定一个flag来确保从00分到01分只进行一次整点报时
部分代码如下:
if(fade_calendar.sec!=calendar.sec)
{
if(fade_calendar.sec/10!=calendar.sec/10)
{
draw_str.Digital_Draw_Line =DrawLine3_CLEAR;
Drawsec1(fade_calendar.sec);
draw_str.Digital_Draw_Line =DrawLine2;
Drawsec1(calendar.sec);
}
if(calendar.min==0 && zd==1)
{
BEEP = 0;
delay_ms(388);
BEEP = 1;
delay_ms(100);
BEEP = 0;
delay_ms(388);
BEEP = 1;
zd=0;
}else if(calendar.min==0 && zd==0)
{
delay_ms(800);
}
else{
delay_ms(800);
zd = 1;
}
}
四,闹钟部分
通过串口设置闹钟,对闹钟结构体赋值,为了lcd刷新流畅,我选择了在主函数里面用结构体判断来进行闹铃发声,部分代码如下
if(ALA_calendar.w_year == calendar.w_year&&ALA_calendar.w_month == calendar.w_month&&ALA_calendar.w_date == calendar.w_date&&ALA_calendar.hour == calendar.hour&&ALA_calendar.min == calendar.min&&ALA_calendar.sec == calendar.sec)
{
BEEP = 0;
if(fade_calendar.sec!=calendar.sec)
{
if(fade_calendar.sec/10!=calendar.sec/10)
{
draw_str.Digital_Draw_Line =DrawLine3_CLEAR;
Drawsec1(fade_calendar.sec);
draw_str.Digital_Draw_Line =DrawLine2;
Drawsec1(calendar.sec);
}
if(calendar.min==0 && zd==1)
{
BEEP = 0;
delay_ms(388);
BEEP = 1;
delay_ms(100);
BEEP = 0;
delay_ms(388);
BEEP = 1;
zd=0;
}else if(calendar.min==0 && zd==0)
{
delay_ms(800);
}
else{
delay_ms(800);
zd = 1;
}
if(fade_calendar.sec%10!=calendar.sec%10)
{
draw_str.Digital_Draw_Line =DrawLine3_CLEAR;
Drawsec2(fade_calendar.sec);
draw_str.Digital_Draw_Line =DrawLine2;
Drawsec2(calendar.sec);
}
}
}