OS:Windows 64
Development kit:MDK5.14
IDE:UV4
MCU:STM32F103C8T6
1、RTC時鍾簡介
STM32 的實時時鍾(RTC)是一個獨立的定時器,在相應軟件配置下,可提供時鍾日歷的功能。 詳細資料請參考ALIENTEK的官方文檔——《STM32F1開發指南(精英版-庫函數版)》,以下為博主摘錄要點:
- RTC 模塊和時鍾配置系統(RCC_BDCR 寄存器)在后備區域 ,系統復位后,會自動禁止訪問后備寄存器和 RTC ,所以在要設置時間之前, 先要取消備份區域(BKP)的寫保護
- RTC 內核完全獨立於 RTC APB1 接口,而軟件是通過 APB1 接口訪問 RTC 的預分頻值、計數器值和鬧鍾值,因此需要等待時鍾同步,寄存器同步標志位(RSF)會硬件置1
- RTC相關寄存器包括:控制寄存器(CRH、CRL)、預分頻裝載寄存器(PRLH、PRLL)、預分頻器余數寄存器(DIVH、DIVL)、計數寄存器(CNTH、CNTL)、鬧鍾寄存器(ALRH、ALRL)
- STM32備份寄存器,存RTC校驗值和一些重要參數,最大字節84,可由VBAT供電
- 計數器時鍾頻率:RTCCLK頻率/(預分頻裝載寄存器值+1)
2、軟硬件設計
由於RTC是STM32芯片自帶的時鍾資源,所以自主開發的時候只需要在設計時加上晶振電路和紐扣電池即可。編程時在HARDWARE文件夾新建 rtc.c、rtc.h 文件。
3、時鍾配置與函數編寫
為了使用RTC時鍾,需要進行配置和時間獲取,基本上按照例程來寫就可以了。為避免零散,我將附上完整代碼。函數說明如下:
RTC_Init(void) | 配置時鍾 |
RTC_NVIC_Config(void) | 中斷分組 |
RTC_IRQHandler(void) | 秒中斷處理 |
RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec) | 設置時間 |
RTC_Alarm_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec) | 鬧鍾設置 |
RTC_Get(void) | 獲取時鍾 |
RTC_Get_Week(u16 year,u8 month,u8 day) | 星期計算 |
Is_Leap_Year(u16 year) | 閏年判斷 |
事實上,以上函數並不都要,鬧鍾沒有用到的話就不要,秒中斷也可以不作處理,看項目需求。

1 #include "sys.h"
2 #include "delay.h"
3 #include "rtc.h"
4
5 _calendar_obj calendar;//時鍾結構體
6
7 static void RTC_NVIC_Config(void) 8 { 9 NVIC_InitTypeDef NVIC_InitStructure; 10 NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn; //RTC全局中斷
11 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //先占優先級1位,從優先級3位
12 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //先占優先級0位,從優先級4位
13 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能該通道中斷
14 NVIC_Init(&NVIC_InitStructure); //根據NVIC_InitStruct中指定的參數初始化外設NVIC寄存器
15 } 16
17 //實時時鍾配置 18 //初始化RTC時鍾,同時檢測時鍾是否工作正常 19 //BKP->DR1用於保存是否第一次配置的設置 20 //返回0:正常 21 //其他:錯誤代碼
22
23 u8 RTC_Init(void) 24 { 25 //檢查是不是第一次配置時鍾
26 u8 temp=0; 27 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); //使能PWR和BKP外設時鍾
28 PWR_BackupAccessCmd(ENABLE); //使能后備寄存器訪問
29 if (BKP_ReadBackupRegister(BKP_DR1) != 0x5051) //從指定的后備寄存器中讀出數據:讀出了與寫入的指定數據不相乎
30 { 31 BKP_DeInit(); //復位備份區域
32 RCC_LSEConfig(RCC_LSE_ON); //設置外部低速晶振(LSE),使用外設低速晶振
33 while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET&&temp<250) //檢查指定的RCC標志位設置與否,等待低速晶振就緒
34 { 35 temp++; 36 delay_ms(10); 37 } 38 if(temp>=250)return 1;//初始化時鍾失敗,晶振有問題
39 RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //設置RTC時鍾(RTCCLK),選擇LSE作為RTC時鍾
40 RCC_RTCCLKCmd(ENABLE); //使能RTC時鍾
41 RTC_WaitForLastTask(); //等待最近一次對RTC寄存器的寫操作完成
42 RTC_WaitForSynchro(); //等待RTC寄存器同步
43 RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能RTC秒中斷
44 RTC_WaitForLastTask(); //等待最近一次對RTC寄存器的寫操作完成
45 RTC_EnterConfigMode();/// 允許配置
46 RTC_SetPrescaler(32767); //設置RTC預分頻的值
47 RTC_WaitForLastTask(); //等待最近一次對RTC寄存器的寫操作完成
48 RTC_Set(2018,4,2,17,37,00); //設置時間
49 RTC_ExitConfigMode(); //退出配置模式
50 BKP_WriteBackupRegister(BKP_DR1, 0X5051); //向指定的后備寄存器中寫入用戶程序數據
51 } 52 else//系統繼續計時
53 { 54
55 RTC_WaitForSynchro(); //等待最近一次對RTC寄存器的寫操作完成
56 RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能RTC秒中斷
57 RTC_WaitForLastTask(); //等待最近一次對RTC寄存器的寫操作完成
58 } 59 RTC_NVIC_Config();//RCT中斷分組設置
60 RTC_Get();//更新時間
61 return 0; //ok
62
63 } 64 //RTC時鍾中斷 65 //每秒觸發一次 66 //extern u16 tcnt;
67 void RTC_IRQHandler(void) 68 { 69 // if (RTC_GetITStatus(RTC_IT_SEC) != RESET)//秒鍾中斷 70 // { 71 // RTC_Get();//更新時間 72 // } 73 // if(RTC_GetITStatus(RTC_IT_ALR)!= RESET)//鬧鍾中斷 74 // { 75 // RTC_ClearITPendingBit(RTC_IT_ALR); //清鬧鍾中斷 76 // RTC_Get(); //更新時間 77 // //printf("Alarm Time:%d-%d-%d %d:%d:%d\n",calendar.w_year,calendar.w_month,calendar.w_date,calendar.hour,calendar.min,calendar.sec);//輸出鬧鈴時間 78 //
79 // }
80 RTC_ClearITPendingBit(RTC_IT_SEC|RTC_IT_OW); //清鬧鍾中斷
81 RTC_WaitForLastTask(); 82 } 83
84
85 //判斷是否是閏年函數 86 //月份 1 2 3 4 5 6 7 8 9 10 11 12 87 //閏年 31 29 31 30 31 30 31 31 30 31 30 31 88 //非閏年 31 28 31 30 31 30 31 31 30 31 30 31 89 //輸入:年份 90 //輸出:該年份是不是閏年.1,是.0,不是
91 u8 Is_Leap_Year(u16 year) 92 { 93 if(year%4==0) //必須能被4整除
94 { 95 if(year%100==0) 96 { 97 if(year%400==0)return 1;//如果以00結尾,還要能被400整除
98 else return 0; 99 }else return 1; 100 }else return 0; 101 } 102
103
104 //設置時鍾 105 //把輸入的時鍾轉換為秒鍾 106 //以1970年1月1日為基准 107 //1970~2099年為合法年份 108 //返回值:0,成功;其他:錯誤代碼. 109 //月份數據表
110 const u8 table_week[12]={0,3,3,6,1,4,6,2,5,0,3,5}; //月修正數據表
111 const u8 mon_table[12]={31,28,31,30,31,30,31,31,30,31,30,31};//平年的月份日期表
112
113 u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec) 114 { 115 u16 t; 116 u32 seccount=0; 117 if(syear<1970||syear>2099)return 1; 118 for(t=1970;t<syear;t++) //把所有年份的秒鍾相加
119 { 120 if(Is_Leap_Year(t))seccount+=31622400;//閏年的秒鍾數
121 else seccount+=31536000; //平年的秒鍾數
122 } 123 smon-=1; 124 for(t=0;t<smon;t++) //把前面月份的秒鍾數相加
125 { 126 seccount+=(u32)mon_table[t]*86400;//月份秒鍾數相加
127 if(Is_Leap_Year(syear)&&t==1)seccount+=86400;//閏年2月份增加一天的秒鍾數
128 } 129 seccount+=(u32)(sday-1)*86400;//把前面日期的秒鍾數相加
130 seccount+=(u32)hour*3600;//小時秒鍾數
131 seccount+=(u32)min*60; //分鍾秒鍾數
132 seccount+=sec;//最后的秒鍾加上去
133
134 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); //使能PWR和BKP外設時鍾
135 PWR_BackupAccessCmd(ENABLE); //使能RTC和后備寄存器訪問
136 RTC_SetCounter(seccount); //設置RTC計數器的值
137
138 RTC_WaitForLastTask(); //等待最近一次對RTC寄存器的寫操作完成
139 return 0; 140 } 141
142 //初始化鬧鍾 143 //以1970年1月1日為基准 144 //1970~2099年為合法年份 145 //syear,smon,sday,hour,min,sec:鬧鍾的年月日時分秒 146 //返回值:0,成功;其他:錯誤代碼.
147 u8 RTC_Alarm_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec) 148 { 149 u16 t; 150 u32 seccount=0; 151 if(syear<1970||syear>2099)return 1; 152 for(t=1970;t<syear;t++) //把所有年份的秒鍾相加
153 { 154 if(Is_Leap_Year(t))seccount+=31622400;//閏年的秒鍾數
155 else seccount+=31536000; //平年的秒鍾數
156 } 157 smon-=1; 158 for(t=0;t<smon;t++) //把前面月份的秒鍾數相加
159 { 160 seccount+=(u32)mon_table[t]*86400;//月份秒鍾數相加
161 if(Is_Leap_Year(syear)&&t==1)seccount+=86400;//閏年2月份增加一天的秒鍾數
162 } 163 seccount+=(u32)(sday-1)*86400;//把前面日期的秒鍾數相加
164 seccount+=(u32)hour*3600;//小時秒鍾數
165 seccount+=(u32)min*60; //分鍾秒鍾數
166 seccount+=sec;//最后的秒鍾加上去 167 //設置時鍾
168 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); //使能PWR和BKP外設時鍾
169 PWR_BackupAccessCmd(ENABLE); //使能后備寄存器訪問 170 //上面三步是必須的!
171
172 RTC_SetAlarm(seccount); 173
174 RTC_WaitForLastTask(); //等待最近一次對RTC寄存器的寫操作完成
175
176 return 0; 177 } 178
179
180 //得到當前的時間 181 //返回值:0,成功;其他:錯誤代碼.
182 u8 RTC_Get(void) 183 { 184 static u16 daycnt=0; 185 u32 timecount=0; 186 u32 temp=0; 187 u16 temp1=0; 188 timecount=RTC_GetCounter(); 189 temp=timecount/86400; //得到天數(秒鍾數對應的)
190 if(daycnt!=temp)//超過一天了
191 { 192 daycnt=temp; 193 temp1=1970; //從1970年開始
194 while(temp>=365) 195 { 196 if(Is_Leap_Year(temp1))//是閏年
197 { 198 if(temp>=366)temp-=366;//閏年的秒鍾數
199 else {temp1++;break;} 200 } 201 else temp-=365; //平年
202 temp1++; 203 } 204 calendar.w_year=temp1;//得到年份
205 temp1=0; 206 while(temp>=28)//超過了一個月
207 { 208 if(Is_Leap_Year(calendar.w_year)&&temp1==1)//當年是不是閏年/2月份
209 { 210 if(temp>=29)temp-=29;//閏年的秒鍾數
211 else break; 212 } 213 else
214 { 215 if(temp>=mon_table[temp1])temp-=mon_table[temp1];//平年
216 else break; 217 } 218 temp1++; 219 } 220 calendar.w_month=temp1+1; //得到月份
221 calendar.w_date=temp+1; //得到日期
222 } 223 temp=timecount%86400; //得到秒鍾數
224 calendar.hour=temp/3600; //小時
225 calendar.min=(temp%3600)/60; //分鍾
226 calendar.sec=(temp%3600)%60; //秒鍾
227 calendar.week=RTC_Get_Week(calendar.w_year,calendar.w_month,calendar.w_date);//獲取星期
228 calendar.msec=(32767-RTC_GetDivider())* 1000/32767; 229 return 0; 230 } 231
232
233 //獲得現在是星期幾 234 //功能描述:輸入公歷日期得到星期(只允許1901-2099年) 235 //輸入參數:公歷年月日 236 //返回值:星期號
237 u8 RTC_Get_Week(u16 year,u8 month,u8 day) 238 { 239 u16 temp2; 240 u8 yearH,yearL; 241
242 yearH=year/100; yearL=year%100; 243 // 如果為21世紀,年份數加100
244 if (yearH>19)yearL+=100; 245 // 所過閏年數只算1900年之后的
246 temp2=yearL+yearL/4; 247 temp2=temp2%7; 248 temp2=temp2+day+table_week[month-1]; 249 if (yearL%4==0&&month<3)temp2--; 250 return(temp2%7); 251 }

1 #include "sys.h"
2
3 //時間結構體
4 typedef struct
5 { 6 vu8 hour;//vu8
7 vu8 min; 8 vu8 sec; 9 vu16 msec; 10
11 //公歷日月年周
12 vu16 w_year; 13 vu8 w_month; 14 vu8 w_date; 15 vu8 week; 16 }_calendar_obj; 17
18 extern _calendar_obj calendar; //日歷結構體
19 extern u8 const mon_table[12]; //月份日期數據表
20
21 u8 RTC_Init(void); //初始化RTC,返回0,失敗;1,成功;
22 u8 Is_Leap_Year(u16 year);//平年,閏年判斷 23
24 //u8 RTC_Alarm_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec);
25 u8 RTC_Get(void); //更新時間
26 u8 RTC_Get_Week(u16 year,u8 month,u8 day); 27 u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec);//設置時間
4、秒鍾計時原理
使用外部32.768KHz的晶振作為時鍾的輸入頻率,設置預分頻裝載寄存器的值為32767,根據計算公式,剛好可以得到1秒的計數頻率。時間基准設置為1970年1月1日0時0分0秒,后續的時間都以這個為基准進行計算。RTC計數器是32位的,理論上可以記錄136年左右的時間。(注意不必在秒中斷里更新時間)
5、毫秒計時原理
如果要獲取到毫秒級的時鍾怎么辦?在我的項目中就有這樣的要求。事實上,獲取毫秒時鍾也非常簡單。
查閱開發指南,RTC預分頻器余數寄存器(RTC_DIVH、RTC_DIVL),這兩個寄存器的作用就是用來獲得比秒鍾更為准確的時鍾。 該寄存器的值自減的,用於保存還需要多少時鍾周期獲得一個秒信號。在一次秒鍾更新后,由硬件重新裝載。這兩個寄存器和 RTC 預分頻裝載寄存器位數是一樣的。也就是說,如果預分頻裝載寄存器的值為32767,那么余數寄存器就會在每一次秒更新時由硬件重新裝載為32767,然后向下計數,計數到0表示一秒,也即1000ms。
因此,我們在時鍾結構體中添加msec成員
1 //時間結構體
2 typedef struct
3 { 4 vu8 hour;//vu8
5 vu8 min; 6 vu8 sec; 7 vu16 msec; 8
9 //公歷日月年周
10 vu16 w_year; 11 vu8 w_month; 12 vu8 w_date; 13 vu8 week; 14 }_calendar_obj;
獲取毫秒時間
1 calendar.msec=(32767-RTC_GetDivider())*1000/32767;
6、修改時間
如果RTC時鍾在使用的過程中不准了(我遇到的情況大概是掉電跑了2個月,重新測試的時候差了2分鍾左右),可以重新校准時鍾。我們在備份區域 BKP_DR1 中寫入 0X5051 ,下次開機(或復位)的時候,先讀取 BKP_DR1 的值,然后判斷是否是 0X5051來決定是不是要配置。 如果要修改時間,請將0x5051改為其它數據,修改RTC_Set函數實參,再重新燒寫一下程序即可。
if (BKP_ReadBackupRegister(BKP_DR1) != 0x5051) { ... RTC_EnterConfigMode();/// 允許配置
RTC_SetPrescaler(32767); //設置RTC預分頻的值
RTC_WaitForLastTask(); //等待最近一次對RTC寄存器的寫操作完成
RTC_Set(2018,4,2,17,37,00); //設置時間
RTC_ExitConfigMode(); //退出配置模式
BKP_WriteBackupRegister(BKP_DR1, 0X5051); //向指定的后備寄存器中寫入用戶程序數據
}