目錄
一、Uinx時間戳
1.1Uinx簡介
1.2UTC/GMT
1.3時間戳轉換
1.3.1主要數據類型
1.3.2主要函數
1.3.3C語言時間戳轉換示例
1.3.4時間格式化說明符
1.3.5注意事項
二、BKP
2.1BKP簡介
2.2BKP基本結構
三、RTC
3.1RTC簡介
3.2RTC框圖
3.3RTC基本結構
3.4RTC硬件電路
3.5RTC操作注意事項
3.6學習中的疑惑與解答:PWR和RTC、BKP之間有什么關系?
3.6.1核心關系一句話總結
3.6.2. 三個模塊各自的角色
3.6.3.它們為什么會聯系在一起?
3.6.4總結與類比
四、BKP讀寫代碼編寫
4.1讀寫BKP步驟
4.2BKP相關庫函數與PWR與之對應的庫函數
4.3讀寫BKP代碼編寫
4.3.1讀寫數組BKP
五、RTC代碼編寫
5.1RTC初始化
5.2RTC相關庫函數
5.3RTC初始化代碼編寫
5.4顯示時間戳代碼
5.5讀寫設置的時間
5.6初始化RTC優化
5.7顯示余數寄存器的數值
一、Uinx時間戳
1.1Uinx簡介
?Unix 時間戳(Unix Timestamp)定義為從UTC/GMT(倫敦時間)的1970年1月1日0時0分0秒開始所經過的秒數,不考慮閏秒?
?時間戳存儲在一個秒計數器中,秒計數器為32位/64位的整型變量
?世界上所有時區的秒計數器相同,不同時區通過添加偏移來得到當地時間
1.2UTC/GMT
?GMT(Greenwich Mean Time)格林尼治標準時間是一種以地球自轉為基礎的時間計量系統。它將地球自轉一周的時間間隔等分為24小時,以此確定計時標準(前)
?UTC(Universal Time Coordinated)協調世界時是一種以原子鐘為基礎的時間計量系統。它規定銫133原子基態的兩個超精細能級間在零磁場下躍遷輻射9,192,631,770周所持續的時間為1秒。當原子鐘計時一天的時間與地球自轉一周的時間相差超過0.9秒時,UTC會執行閏秒(多加秒數)來保證其計時與地球自轉的協調一致(后)
1.3時間戳轉換
C語言的time.h模塊提供了時間獲取和時間戳轉換的相關函數,可以方便地進行秒計數器、日期時間和字符串之間的轉換
1.3.1主要數據類型
time_t
:通常是一個整數類型,用于存儲時間戳
struct tm
:用于存儲日期和時間成分的結構體
clock_t
:用于測量處理器時間的類型
1.3.2主要函數
函數 | 作用 |
time_t time(time_t*); | 獲取系統時鐘(自己設備上的時間) |
struct tm* gmtime(const time_t*); | 秒計數器轉換為日期時間(格林尼治時間) |
struct tm* localtime(const time_t*); | 秒計數器轉換為日期時間(當地時間) |
time_t mktime(struct tm*); | 日期時間轉換為秒計數器(當地時間) |
char* ctime(const time_t*); | 秒計數器轉換為字符串(默認格式) |
char* asctime(const struct tm*); | 日期時間轉換為字符串(默認格式) |
size_t strftime(char*, size_t, const char*, const struct tm*); | 日期時間轉換為字符串(自定義格式) |
1.3.3C語言時間戳轉換示例
#include <stdio.h>
#include <time.h>
#include <stdlib.h>int main()
{printf("========== 時間戳轉換示例 ==========\n\n");// 獲取當前時間戳time_t current_time = time(NULL);printf("1. 當前時間戳: %ld\n", current_time);printf(" (從1970年1月1日00:00:00 UTC開始的秒數)\n\n");// 將時間戳轉換為本地時間結構體struct tm* local_time = localtime(¤t_time);printf("2. 本地時間分解:\n");printf(" 年份: %d (從1900年開始計算,實際年份=%d)\n",local_time->tm_year, local_time->tm_year + 1900);printf(" 月份: %d (0-11, 0=1月,實際月份=%d)\n",local_time->tm_mon, local_time->tm_mon + 1);printf(" 日: %d\n", local_time->tm_mday);printf(" 時: %d\n", local_time->tm_hour);printf(" 分: %d\n", local_time->tm_min);printf(" 秒: %d\n", local_time->tm_sec);printf(" 星期: %d (0-6, 0=周日)\n", local_time->tm_wday);printf(" 年中的第幾天: %d\n\n", local_time->tm_yday);// 將時間戳轉換為UTC時間結構體struct tm* utc_time = gmtime(¤t_time);printf("3. UTC時間分解:\n");printf(" 年份: %d (實際年份=%d)\n",utc_time->tm_year, utc_time->tm_year + 1900);printf(" 月份: %d (實際月份=%d)\n",utc_time->tm_mon, utc_time->tm_mon + 1);printf(" 日: %d\n", utc_time->tm_mday);printf(" 時: %d\n", utc_time->tm_hour);printf(" 分: %d\n", utc_time->tm_min);printf(" 秒: %d\n\n", utc_time->tm_sec);// 使用strftime格式化時間輸出char formatted_time[100];strftime(formatted_time, sizeof(formatted_time),"4. 格式化時間: %Y年%m月%d日 %H時%M分%S秒", local_time);printf("%s\n", formatted_time);strftime(formatted_time, sizeof(formatted_time),"5. 另一種格式: %A, %B %d, %Y %I:%M:%S %p", local_time);printf("%s\n\n", formatted_time);// 將struct tm轉換回時間戳time_t converted_time = mktime(local_time);printf("6. 轉換回的時間戳: %ld\n", converted_time);printf("========== 程序結束 ==========\n");return 0;
}
#char* ctime(const time_t*);這個函數VS顯示函數不安全,我就沒展示#
// 將時間戳轉換為可讀字符串printf("可讀時間字符串: %s\n", ctime(¤t_time));
1.3.4時間格式化說明符
在strftime
函數中常用的格式說明符
%Y
:4位數的年份
%y
:2位數的年份
%m
:月份(01-12)
%d
:日(01-31)
%H
:24小時制的小時(00-23)
%I
:12小時制的小時(01-12)
%M
:分鐘(00-59)
%S
:秒(00-59)
%p
:AM/PM指示
%A
:完整的星期名稱
%B
:完整的月份名稱
1.3.5注意事項
struct tm
中的年份是從1900年開始計算的,所以需要加1900
struct tm
中的月份是從0開始計數的(0=1月,11=12月),所以要加1,顯示目前月份
mktime()
函數會自動調整struct tm
中的超出范圍的值(如將60秒調整為下一分鐘)時區轉換需要注意使用
localtime()
(本地時間)或gmtime()
(UTC時間)
二、BKP
2.1BKP簡介
?BKP(Backup Registers)備份寄存器
?BKP可用于存儲用戶應用程序數據。當VDD(2.0~3.6V)主電源被切斷,他們仍然由VBAT(備用電池,引腳1)(1.8~3.6V)維持供電。當系統在待機模式下被喚醒,或系統復位或電源復位時,他們也不會被復位
RTC和BKP共享同一個備用電源域。這意味著,只要你的板子上有后備電池,無論是RTC的計時,還是BKP中存儲的數據,在主板斷電后都不會丟失。
若VDD和VBAT都斷電,則BKP內數據丟失(BKP本質是RAM存儲器)
?TAMPER引腳(引腳2 默認復用功能 安全保障設計)產生的侵入事件將所有備份寄存器內容清除
?RTC引腳輸出RTC校準時鐘、RTC鬧鐘脈沖或者秒脈沖
?存儲RTC時鐘校準寄存器
?用戶數據存儲容量:20字節(中容量和小容量)/ 84字節(大容量和互聯型)
2.2BKP基本結構
圖中橙色部分為后備區域,BKP和RTC處于后備區域,其特性是:VDD主電源掉電時,后備區域仍可以由VBAT的備用電池供電;VDD主電源上電時,后備區域供電會由VBAT切換到VDD(節省電池電量)
BKP含有:數據寄存器、控制寄存器、狀態寄存器、RTC校準寄存器
a) 數據寄存器(BKP_DRx - Data Register)
這是BKP的核心,用于存儲用戶數據。不同型號的STM32,數據寄存器的數量不同
-
命名:
BKP_DR1
,?BKP_DR2
,?BKP_DR3
, ...?BKP_DRn
-
寬度:每個寄存器都是?16位?寬,可以存儲2個字節,小/中容量有10個寄存器,大容量/互聯型設備有42個數據寄存器。
-
功能:你可以用它們存儲任何你想在斷電后保留的數據,例如:
-
RTC配置狀態標志
-
系統配置參數
-
運行日志或錯誤代碼
-
校準值
-
b) 控制與狀態寄存器(BKP_CR / BKP_CSR)
這些寄存器用于管理BKP模塊本身的功能,例如:
-
TAMPER引腳檢測:許多STM32有一個防篡改(Tamper)引腳。當這個引腳上有指定電平跳變時,它可以自動擦除整個BKP區域的數據(出于安全考慮)。相關寄存器用于配置檢測邊沿和使能該功能。
-
RTC校準:有些寄存器位用于對RTC時鐘進行精細的軟件校準。
三、RTC
3.1RTC簡介
?RTC(Real Time Clock)實時時鐘
?RTC是一個獨立的定時器,可為系統提供時鐘和日歷的功能
?RTC和時鐘配置系統處于后備區域,系統復位時數據不清零,VDD(2.0~3.6V)斷電后可借助VBAT(1.8~3.6V)供電繼續走時
?32位的可編程計數器,可對應Unix時間戳的秒計數器
?20位的可編程預分頻器,可適配不同頻率的輸入時鐘
?可選擇三種RTC時鐘源:
? HSE時鐘除以128(通常為8MHz/128)
? LSE振蕩器時鐘(通常為32.768KHz)
? LSI振蕩器時鐘(40KHz)
3.2RTC框圖
3.3RTC基本結構
①RTCCLK時鐘來源,RCC內配置(數據選擇器黃色部分,選擇時鐘來源)
時鐘源選擇器 (RTCSEL)
框圖入口。RTC可以選擇三個時鐘源之一:
LSE:首選。外部低速晶振,精度高。
LSI:內部低速RC振蕩器,成本低但精度較差,受溫度影響大。
HSE/128:高速外部時鐘分頻后的信號,精度高但耗電,主電源掉電后無法使用。
通過?RCC_BDCR?寄存器中的?
RTCSEL
?位進行選擇。這個選擇是一次性的,只有在備份域復位后才能重新設置。
②預分頻器:余數寄存器——自減計數器,存儲當前計數值;重裝寄存器——計數目標,決定分頻值,之后得到1Hz秒計數信號(配置重裝寄存器選擇分頻系數)
預分頻器 (Prescaler)
作用:將輸入的高速時鐘(如32.768kHz)分頻,得到精確的1Hz(每秒一次)信號。
組成:通常由一個20位的異步預分頻器和一個7位的同步預分頻器組成。異步分頻器降低時鐘頻率以降低功耗,同步分頻器保證與APB1時鐘域的無風險交互。
計算:例如,使用LSE(32768 Hz)時,通常設置異步分頻器為
127
,同步分頻器為255
,則最終頻率為:?32768 / (127 + 1) / (255 + 1) = 1 Hz
。
③計數器:32位計數器——1s自增一次;32位鬧鐘值——設定鬧鐘(配置32位寄存器,進行日期時間的讀寫)
32位可編程計數器 (RTC_CNT)
這是RTC的核心。它是一個由預分頻器輸出的1Hz信號驅動的32位向上計數器。
它存儲的值就是從參考起點開始所經過的秒數。如果我們把參考起點設置為Unix紀元(1970-01-01 00:00:00),那么這個計數器的值就是當前的Unix時間戳。
軟件可以直接讀寫這個計數器來設置或獲取時間。
鬧鐘寄存器 (RTC_ALR)
一個32位的比較寄存器。你可以設置一個想要的秒數目標值。
當RTC_CNT的值與RTC_ALR的值匹配時,就會產生一個鬧鐘中斷。你可以利用這個中斷讓單片機在特定時間執行某些任務(比如喚醒停止模式)。
④三個信號觸發中斷:秒信號、計數器溢出信號、鬧鐘信號,通過中斷輸出控制,進行中斷使能
⑤使能的中斷,通向NVIC,向CPU申請中斷(需要中斷,先允許中斷,再配置NVIC,再寫對應的中斷函數)
控制與狀態寄存器
用于管理RTC的各種功能,如:
使能寄存器 (RTC_CRH/CRL):允許配置和產生秒中斷、鬧鐘中斷等。
狀態寄存器:用于查看各種中斷標志位。
3.4RTC硬件電路
a) 主電源 (VDD)
-
作用:為STM32芯片的主核心(包括RTC模塊的邏輯部分)供電。
-
要求:當主電源存在時,RTC由VDD供電。
b) 備用電池 (VBAT)
-
作用:這是RTC的“生命線”。當主電源VDD斷開時,自動切換由VBAT引腳上的電池為整個備份域(包括RTC計數器和BKP寄存器)供電,保持計時和數據不丟失。
-
典型選擇:一顆3V的紐扣電池(如CR2032)。其續航能力可達數年。
-
電路設計:通常會在VBAT路徑上串聯一個肖特基二極管,并在VDD路徑上也串聯一個二極管。這樣可以實現電源自動切換:當VDD電壓高于VBAT時,由VDD供電;當VDD掉電時,由VBAT供電,防止電流倒灌。或者直接連接一個1.8~3.6V的電池到VBAT,內有一個供電開關,在不同情況選擇合適供電。
c) 低速外部晶振 (LSE)
-
作用:為RTC提供高精度的時鐘源。RTC的本質是一個計數器,它需要一個“心跳”來遞增,LSE就是這個“心跳”。
-
頻率:32.768 kHz。這是一個標準頻率,因為?
32768 = 2^15
,經過一個15位的分頻器(2^15
分頻)后,正好得到?1Hz(每秒一次)的信號,非常適合驅動秒計數器。 -
電路設計:需要連接一個32.768kHz的晶振(Xtal),并搭配兩個負載電容(通常為5~15pF)。
3.5RTC操作注意事項
?執行以下操作將使能對BKP和RTC的訪問:
? 設置RCC_APB1ENR(開啟APB1外設的時鐘)的PWREN和BKPEN,使能PWR和BKP時鐘
? 設置PWR_CR的DBP,使能對BKP和RTC的訪問(調用PWR庫函數)
?若在讀取RTC寄存器時,RTC的APB1接口曾經處于禁止狀態,則軟件首先必須等待RTC_CRL寄存器中的RSF位(寄存器同步標志)被硬件置1。(調用等待同步函數)
根本原因:時鐘域不同。RTC核心由低速的LSE(32.768kHz)驅動,而CPU和其寄存器接口由高速的APB1總線時鐘驅動。這兩個時鐘域是異步的。
問題所在:當APB1接口被禁用(例如系統剛復位、或從低功耗模式喚醒)后重新啟用時,RTC的日歷寄存器組(
RTC_CNT
,?RTC_ALR
,?RTC_PRL
)需要從RTC的時鐘域同步到APB1的時鐘域。在這個同步過程完成之前,軟件讀取到的寄存器值可能是陳舊、不可靠的。RSF位的作用:
RSF
?(Register Synchronized Flag) 位由硬件自動置1,標志著同步過程已完成。軟件必須等待此位被置1后,才能安全地讀取日歷寄存器,以確保數據的正確性。
?必須設置RTC_CRL寄存器中的CNF位,使RTC進入配置模式后,才能寫入RTC_PRL、RTC_CNT、RTC_ALR寄存器
保證寫的原子性。
RTC_PRL
(預分頻器)、RTC_CNT
(計數器)和RTC_ALR
(鬧鐘值)這三個寄存器是密切相關的。為了防止在軟件修改其中一個寄存器時,RTC核心正在更新另一個寄存器而導致數據不一致(例如,在修改秒計數器的高16位時,低16位可能因為秒信號到來而遞增),STM32引入了“配置模式”的概念。CNF位的作用:將?
CNF
?(Configuration Flag) 位置1,會使RTC核心暫停對日歷寄存器的更新,并允許軟件一次性、安全地寫入多個相關寄存器。退出配置模式后,RTC核心會使用新值繼續運行。
?對RTC任何寄存器的寫操作,都必須在前一次寫操作結束后進行。可以通過查詢RTC_CR寄存器中的RTOFF狀態位,判斷RTC寄存器是否處于更新中。僅當RTOFF狀態位是1時,才可以寫入RTC寄存器。(調用等待函數)
根本原因:慢速外設。RTC的時鐘源非常慢(RTCCLK~32kHz),而CPU的指令執行速度極快(PCLK1~72MHz)。向RTC寄存器寫入一個數據需要數個RTC時鐘周期才能完成。
問題所在:如果軟件在前一次寫操作尚未被RTC硬件處理完成時,就立刻發起下一次寫操作,會導致后一次寫操作被忽略,或者造成寄存器內容錯誤。
RTOFF位的作用:
RTOFF
?(RTC Operation OFF) 位由硬件控制。當它為?0?時,表示RTC正在處理上一次的寫操作,此時總線會被鎖定,禁止新的寫入。當它為?1?時,表示RTC空閑,允許新的寫操作。
3.6學習中的疑惑與解答:PWR和RTC、BKP之間有什么關系?
3.6.1核心關系一句話總結
PWR(電源控制)是基礎和保障,而RTC(實時時鐘)和BKP(備份寄存器)是需要被保護的核心數據。PWR確保了即使在主電源(VDD)斷開的情況下,也能通過備用電池(VBAT)為RTC和BKP所在的“備份域”持續供電,從而保住這些關鍵數據不丟失。
3.6.2. 三個模塊各自的角色
a) PWR - 電源控制
-
職責:管理整個微控制器的電源,包括供電、功耗模式(如睡眠、停止、待機模式)以及電源域的劃分。
-
關鍵概念:STM32內部有兩個主要的電源域:
-
VDD域:由主電源(VDD)供電。絕大部分外設(如GPIO、USART、SPI)和內核(CPU)、SRAM都在這個域。當主電源斷開時,這個域的內容會丟失。
-
備份域:由一個單獨的引腳VBAT供電。即使主電源VDD斷開,只要VBAT引腳上接有電池(如3V紐扣電池),這個域就能繼續工作。
-
b) BKP - 備份寄存器
-
職責:提供一小塊在備份域中的特殊內存(通常是16-20個16位的寄存器)。
-
關鍵特性:只要備份域有電(無論是來自VDD還是VBAT),這些寄存器里的數據就不會丟失。因此,它們常用來存儲系統配置、密碼、運行次數等需要掉電保存的關鍵數據。
-
訪問控制:為了防止意外寫入,對BKP寄存器的訪問受到PWR_CR寄存器中的DBP位控制。只有先使能
DBP
位,才能讀寫BKP寄存器。
c) RTC - 實時時鐘
-
職責:提供一個獨立的計時器/日歷功能。
-
關鍵特性:RTC的核心也是一個需要持續運行的部件。因此,它也被放在了備份域中。這樣,無論主系統是否上電,只要VBAT有電,RTC就能一直“滴答滴答”地走時。
3.6.3.它們為什么會聯系在一起?
因為RTC和BKP同屬于“備份域”。
而PWR模塊,是進入和管理這個“備份域”的“看門人”和“電源開關”。
當你學習RTC和BKP時,你必然要操作備份域里的資源。而要安全地操作這些資源,就必須通過PWR模塊來進行配置。這就是為什么在講BKP/RTC的章節,會突然引入PWR。
它們的關系結構圖如下:
從這個圖可以看出:
-
物理供電:PWR模塊負責選擇是使用VDD還是VBAT為備份域供電。
-
邏輯訪問:PWR模塊中的
DBP
位是讀寫BKP和RTC部分寄存器的“鑰匙”。
3.6.4總結與類比
你可以把一個帶STM32的設備想象成一個公司:
-
BKP:是公司的保險柜,里面放著最重要的現金和合同(關鍵數據)。
-
RTC:是公司墻上那個永遠不停的電子鐘,即使公司下班斷電了,它靠自己的電池也能走(備份域供電)。
-
PWR:是公司的物業和電力部門。它管理著整個大樓的供電(主電源和備用發電機),并且掌握著打開保險柜房間的鑰匙(
DBP
位)。 -
Tamper引腳:是保險柜上的震動警報器。一旦有人動保險柜(觸發入侵),警報器就會通知電力部門(PWR),電力部門會啟動應急 protocol(喚醒系統),并且警報器會指令保險柜自毀(擦除BKP數據)。
四、BKP讀寫代碼編寫
4.1讀寫BKP步驟
①BKP初始化:開啟PWREN和BKPEN的時鐘;使用PWR_CR的一個函數,使能BKP和RTC的訪問
②寫DR:BKP寫函數
③讀DR:BKP讀函數
4.2BKP相關庫函數與PWR與之對應的庫函數
1.恢復缺省配置(手動清空BKP所有的數據寄存器)
void BKP_DeInit(void);
下面2個TAMPER函數是配置TAMPER侵入檢測功能
2.配置TAMPER引腳的有效電平(高/低電平觸發)
void BKP_TamperPinLevelConfig(uint16_t BKP_TamperPinLevel);
3.TAMPER使能(是否開啟侵入檢測功能:若需要此項功能,需要先配置有效電平在配置使能)
void BKP_TamperPinCmd(FunctionalState NewState);
4.中斷配置:是否開啟中斷
void BKP_ITConfig(FunctionalState NewState);
5.時鐘輸出功能配置(可選擇在RTC引腳輸出時鐘信號,輸出RTC校準時鐘、RTC鬧鐘脈沖、秒脈沖)
void BKP_RTCOutputConfig(uint16_t BKP_RTCOutputSource);
6.設置RTC校準值(寫入RTC校準寄存器)
void BKP_SetRTCCalibrationValue(uint8_t CalibrationValue);
?下面2個是BKP的重點函數
7.寫備份寄存器
void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);
8.讀備份寄存器
uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);下面4個是獲取標志和清除標志的函數
FlagStatus BKP_GetFlagStatus(void);
void BKP_ClearFlag(void);
ITStatus BKP_GetITStatus(void);
void BKP_ClearITPendingBit(void);
PWR與BKP相關的庫函數:
?1.備份寄存器訪問使能,設置PWR_CR寄存器內的DBP位,使能對BKP和RTC的訪問
?? ?void PWR_BackupAccessCmd(FunctionalState NewState);
4.3讀寫BKP代碼編寫
//①使能BKP和PWRRCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);//②使能對BKP和RTC的訪問PWR_BackupAccessCmd(ENABLE);BKP_WriteBackupRegister(BKP_DR1,0x1234);OLED_ShowHexNum(1,1,BKP_ReadBackupRegister(BKP_DR1),4);while(1){}
4.3.1讀寫數組BKP
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "key.h"
uint16_t WriteARR[]={0x1234,0x5678};
uint16_t ReadARR[2];
uint8_t key;
void key_pro(void)
{key=key_init();if(key==1){WriteARR[0]++;WriteARR[1]++;BKP_WriteBackupRegister(BKP_DR1,WriteARR[0]);BKP_WriteBackupRegister(BKP_DR2,WriteARR[1]);OLED_ShowHexNum(1,3,WriteARR[0],4);OLED_ShowHexNum(1,8,WriteARR[1],4);}}int main(void)
{ OLED_Init();key_scan();//初始化//①使能BKP和PWRRCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);//②使能對BKP和RTC的訪問PWR_BackupAccessCmd(ENABLE);while(1){OLED_ShowString(1,1,"W:");OLED_ShowString(2,1,"R:");key_pro();ReadARR[0]=BKP_ReadBackupRegister(BKP_DR1);ReadARR[1]=BKP_ReadBackupRegister(BKP_DR2);OLED_ShowHexNum(2,3,ReadARR[0],4);OLED_ShowHexNum(2,8,ReadARR[1],4);}
}
五、RTC代碼編寫
5.1RTC初始化
RTC初始化流程:
①開啟PWR和BKP的時鐘
②使能BKP和RTC的訪問
③啟動RTC時鐘,選擇LSE作為系統時鐘(LES需要手動開啟,平時為了省電是關閉狀態)(RCC內部)
④配置RTCCLK數據選擇器,指定LSE作為RTCCLK(RCC模塊內)
⑤調用等待函數:等待同步和等待上一次操作完成
⑥配置預分頻器,給PRC重裝寄存器一個合適的預分頻值,以確保輸出給寄存器的頻率是1Hz
⑦配置CNT的值,給RTC初始時間
⑧(若需要配置鬧鐘值)
⑨(若需要中斷,配置中斷:NVIC)
?
5.2RTC相關庫函數
RCC模塊內有關RTC庫函數:
?1.配置LSE外部低速時鐘,啟動LSE時鐘
void RCC_LSEConfig(uint8_t RCC_LSE);
2.配置LSI內部低速時鐘(若外部時鐘不起振,可使用內部時鐘進行驗證)
void RCC_LSICmd(FunctionalState NewState);
?3.RTCCLK配置(選擇RTCCLK的時鐘源)
void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource);
?4.啟動RTCCLK(調用配置RTCCLK函數之后,還需使能RTCCLK)
void RCC_RTCCLKCmd(FunctionalState NewState);
?5.獲取標志位(調用啟動時鐘后,需等待標志位LSERDY=1,時鐘才算啟動完成并且工作穩定)
FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG);RTC時鐘源初始化庫函數使用:1→5→3→4
RTC庫函數
1.配置中斷輸出
void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState);
2.進入配置模式(置CRL的CNF=1,進入配置模式后,才可讀寫一些寄存器)
void RTC_EnterConfigMode(void);
3.退出配置模式(CNF=0)
void RTC_ExitConfigMode(void);
4.獲取CNT計數器的值(讀取時鐘)
uint32_t ?RTC_GetCounter(void);
5.寫入CNT計數器的值(設置時間)
void RTC_SetCounter(uint32_t CounterValue);
?6.寫入預分頻器(值寫入到預分頻器的PRL重裝寄存器,配置預分頻器的預分頻系數)
void RTC_SetPrescaler(uint32_t PrescalerValue);
7.寫入鬧鐘值(配置鬧鐘)
void RTC_SetAlarm(uint32_t AlarmValue);
8.讀取預分頻器中的DIV余數寄存器(自減計數器——為了獲得更細致的時間)
uint32_t ?RTC_GetDivider(void);
?9.等待上次操作完成(循環,等待RTOFF=1)
void RTC_WaitForLastTask(void);
?10.等待同步(清除RSF標志位,循環直到RSF=1)
void RTC_WaitForSynchro(void);(讀取RTC寄存器之前,時鐘源選擇完畢后,先等待同步10和等待上次操作完成9)
(對RTC任何寄存器的寫操作,都必須在前一次寫操作結束后進行,即每次進行寫操作后都加一個等待上次操作完成函數)
11.獲取標志位,清楚標志位的函數
FlagStatus RTC_GetFlagStatus(uint16_t RTC_FLAG);
void RTC_ClearFlag(uint16_t RTC_FLAG);
ITStatus RTC_GetITStatus(uint16_t RTC_IT);
void RTC_ClearITPendingBit(uint16_t RTC_IT);
?
5.3RTC初始化代碼編寫
#include "stm32f10x.h" // Device headervoid MyRTC_Init(void)
{RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);PWR_BackupAccessCmd(ENABLE);RCC_LSEConfig(RCC_LSE_ON);//啟動LSE振蕩器/* * @arg RCC_LSE_OFF: LSE oscillator OFF LSE振蕩器關閉* @arg RCC_LSE_ON: LSE oscillator ON LSE振蕩器開啟* @arg RCC_LSE_Bypass: LSE oscillator bypassed with external clock LSE時鐘旁路——不接晶振,直接從OSE32——IN引腳輸入一個指定頻率的信號*/while(RCC_GetFlagStatus(RCC_FLAG_LSERDY)!=SET);//等待LSE啟動完成RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);//選擇RTCCLK時鐘源RCC_RTCCLKCmd(ENABLE);RTC_WaitForSynchro();RTC_WaitForLastTask();//⑤調用等待函數:等待同步和等待上一次操作完成RTC_SetPrescaler(32768-1);//⑥配置預分頻器,給PRC重裝寄存器一個合適的預分頻值,以確保輸出給寄存器的頻率是1HzRTC_WaitForLastTask();//對RTC的任何一個寫操作,必須在前一次寫操作結束后進行
//必須設置RTC_CRL寄存器中的CNF位,使RTC進入配置模式后,才能寫入RTC_PRL、RTC_CNT、RTC_ALR寄存器
//寫入預分頻器也需要進入配置模式,但是代碼不需要寫,內部自帶進入退出配置代碼RTC_SetCounter(1672588795);//⑦配置CNT的值,給RTC初始時間,此為2023年,不初始也可以,默認從0開始,一開始是1970年RTC_WaitForLastTask();}
5.4顯示時間戳代碼
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "My_RTC.h"int main(void)
{ OLED_Init();MyRTC_Init();while(1){OLED_ShowNum(1, 1, RTC_GetCounter(), 10); //顯示32位的秒計數器}
}
5.5讀寫設置的時間
#include "stm32f10x.h" // Device header
#include "Time.h"//存放年月日時分秒
uint16_t MyRTC_Time[]={2024,8,26,10,9,30};//設置時間
void MyRTC_SetTime(void)
{/*①將數組時間填充到struct tm 結構體內;②使用mktime函數得到秒數③將秒數寫入RTC的CNT中*/time_t time_cnt;struct tm time_date;time_date.tm_year=MyRTC_Time[0]-1900;time_date.tm_mon=MyRTC_Time[1]-1;time_date.tm_mday=MyRTC_Time[2];time_date.tm_hour=MyRTC_Time[3];time_date.tm_min=MyRTC_Time[4];time_date.tm_sec=MyRTC_Time[5];time_cnt=mktime(&time_date)-8*60*60;//讀取的是倫敦時間的時間戳,-8個小時,就是北京時間的時間戳RTC_SetCounter(time_cnt);RTC_WaitForLastTask();
}//讀取時間
void MyRTC_ReadTime(void)
{time_t time_cnt;struct tm time_date;time_cnt=RTC_GetCounter()+8*60*60;//此時計數是倫敦時間,+8個小時,就是北京時間的時間戳time_date=*localtime(&time_cnt);MyRTC_Time[0]=time_date.tm_year+1900;MyRTC_Time[1]=time_date.tm_mon+1;MyRTC_Time[2]=time_date.tm_mday;MyRTC_Time[3]=time_date.tm_hour;MyRTC_Time[4]=time_date.tm_min;MyRTC_Time[5]=time_date.tm_sec;
}void MyRTC_Init(void)
{RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);PWR_BackupAccessCmd(ENABLE);RCC_LSEConfig(RCC_LSE_ON);//啟動LSE振蕩器while(RCC_GetFlagStatus(RCC_FLAG_LSERDY)!=SET);//等待LSE啟動完成RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);//選擇RTCCLK時鐘源RCC_RTCCLKCmd(ENABLE);RTC_WaitForSynchro();RTC_WaitForLastTask();//⑤調用等待函數:等待同步和等待上一次操作完成RTC_SetPrescaler(32768-1);//⑥配置預分頻器,給PRC重裝寄存器一個合適的預分頻值,以確保輸出給寄存器的頻率是1HzRTC_WaitForLastTask();//對RTC的任何一個寫操作,必須在前一次寫操作結束后進行//必須設置RTC_CRL寄存器中的CNF位,使RTC進入配置模式后,才能寫入RTC_PRL、RTC_CNT、RTC_ALR寄存器//寫入預分頻器也需要進入配置模式,但是代碼不需要寫,內部自帶進入退出配置代碼RTC_SetCounter(1672588795);//⑦配置CNT的值,給RTC初始時間,此為2023年,不初始也可以,默認從0開始,一開始是1970年RTC_WaitForLastTask(); MyRTC_SetTime();}
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "My_RTC.h"int main(void)
{ OLED_Init();MyRTC_Init();OLED_ShowString(1, 1, "Date:XXXX-XX-XX");OLED_ShowString(2, 1, "Time:XX:XX:XX");OLED_ShowString(3, 1, "CNT :");while(1){MyRTC_ReadTime();//顯示MyRTC_Time數組中的時間值OLED_ShowNum(1, 6, MyRTC_Time[0], 4);//年OLED_ShowNum(1, 11, MyRTC_Time[1], 2);//月OLED_ShowNum(1, 14, MyRTC_Time[2], 2);//日OLED_ShowNum(2, 6, MyRTC_Time[3], 2);//時OLED_ShowNum(2, 9, MyRTC_Time[4], 2);//分OLED_ShowNum(2, 12, MyRTC_Time[5], 2);//秒OLED_ShowNum(3, 6, RTC_GetCounter(), 10); //顯示32位的秒計數器}
}
5.6初始化RTC優化
防止電源斷電備用電源不斷電或者按復位鍵時,時間重置
簡單來說,就是在BKP設置一個標志位
void MyRTC_Init(void)
{RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);PWR_BackupAccessCmd(ENABLE);RCC_LSEConfig(RCC_LSE_ON);//啟動LSE振蕩器while(RCC_GetFlagStatus(RCC_FLAG_LSERDY)!=SET);//等待LSE啟動完成RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);//選擇RTCCLK時鐘源RCC_RTCCLKCmd(ENABLE);//第一次上電或者系統完全斷電之后,BKP_DR1默認是0,if成立,執行初始化if(BKP_ReadBackupRegister(BKP_DR1)!=0xFFFF){RTC_WaitForSynchro();RTC_WaitForLastTask();//⑤調用等待函數:等待同步和等待上一次操作完成RTC_SetPrescaler(32768-1);//⑥配置預分頻器,給PRC重裝寄存器一個合適的預分頻值,以確保輸出給寄存器的頻率是1HzRTC_WaitForLastTask();//對RTC的任何一個寫操作,必須在前一次寫操作結束后進行//必須設置RTC_CRL寄存器中的CNF位,使RTC進入配置模式后,才能寫入RTC_PRL、RTC_CNT、RTC_ALR寄存器//寫入預分頻器也需要進入配置模式,但是代碼不需要寫,內部自帶進入退出配置代碼RTC_SetCounter(1672588795);//⑦配置CNT的值,給RTC初始時間,此為2023年,不初始也可以,默認從0開始,一開始是1970年RTC_WaitForLastTask(); MyRTC_SetTime();BKP_WriteBackupRegister(BKP_DR1,0xFFFF);}else{RTC_WaitForSynchro();RTC_WaitForLastTask();//⑤調用等待函數:等待同步和等待上一次操作完成}}
5.7顯示余數寄存器的數值
#之前一直CNT數值不變,但是加上DIV后,數值就自增了,好奇怪,沒理解#
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "My_RTC.h"int main(void)
{ OLED_Init();MyRTC_Init();OLED_ShowString(1, 1, "Date:XXXX-XX-XX");OLED_ShowString(2, 1, "Time:XX:XX:XX");OLED_ShowString(3, 1, "CNT :");OLED_ShowString(4, 1, "DIV :");while(1){MyRTC_ReadTime();//顯示MyRTC_Time數組中的時間值OLED_ShowNum(1, 6, MyRTC_Time[0], 4);//年OLED_ShowNum(1, 11, MyRTC_Time[1], 2);//月OLED_ShowNum(1, 14, MyRTC_Time[2], 2);//日OLED_ShowNum(2, 6, MyRTC_Time[3], 2);//時OLED_ShowNum(2, 9, MyRTC_Time[4], 2);//分OLED_ShowNum(2, 12, MyRTC_Time[5], 2);//秒OLED_ShowNum(3, 6, RTC_GetCounter(), 10); //顯示32位的秒計數器OLED_ShowNum(4, 6, RTC_GetDivider(), 10); //顯示余數寄存器的數值}
}
?DIV數值一直自減,數值范圍時32767~0
?DIV每自減一輪,CNT+1
?因此可以對秒數進行更細致的劃分,秒-分秒-厘秒-毫秒
?? ??? ?若轉換為毫秒,1秒=1000毫秒
?? ??? ?將數值范圍32767——0線性變換為0——999
OLED_ShowNum(4, 6, (32767-RTC_GetDivider())/32767.0*999, 10);?? ?//顯示余數寄存器的數值