Mikrokontrolery STM32 niewątpliwie mają wiele miażdżących ficzerów. Oczywiście w porównaniu do leciwych AVRów, które są nadal stosowane w najpopularniejszym Arduino Uno. Jedną z takich rzeczy jest wbudowany w STMy jest zegar czasu rzeczywistego, w skrócie RTC. Pozwól, że zaprezentuję Ci szczegółowo jak wygląda praca z takim zegarem przy pomocy biblioteki HAL generowanej przez CubeMX.
Jakiś czas temu opisywałem zewnętrzne układy zegarów(DS3231, DS1307 i PCF8563). Podłączane były one przez interfejs I²C, generowały przerwania co sekundę. W zależności od układu posiadały czy to pamięć SRAM podtrzymywaną baterią czy na przykład korektę sygnału z oscylatora dzięki czemu były ekstremalnie dokładne. Miały też w sobie sprzętową obsługę daty.
W pierwszej kolejności zajmę się RTC wbudowanym w układ STM32F103C8T6, czyli w popularnym BluePill’u. Mniej lub bardziej świadomie jest to pierwszy wybór osób początkujących.
Trzeba pamiętać, że jest to jeden z pierwszych układów RTC stosowany w STM32. Nie ma on zbyt wielu funkcji ani rozbudowanych rejestrów, o czym zaraz się przekonasz.
RTC w STM32F103
Układ zegara w jego podstawowym przeznaczeniu ma mierzyć czas, ten taki ludzki, zegarkowy. W F103 do liczenia czasu/daty służy tylko jeden licznik. Jest to 32-bitowy rejestr, który zlicza jedynie ilość sekund. Mówiłem, że jest to prosty RTC, prawda?
No dobra, ale co nam po samych sekundach?! My chcemy mieć godziny, minuty, a nawet datę jak miało to miejsce przy zewnętrznych układach dedykowanych. Co za badziewie…
To prawda, że ten jeden licznik na pierwszy rzut oka może być idiotyczny. Jednak nie jest to takie głupie jakby się wydawało. Nie wiem, czy wiesz, ale w systemach z rodziny Linux również w ten sposób liczone są czas i data.
Dzięki temu, że jest to zrealizowane właśnie na jednym liczniku, programista ma dowolność co do decyzji w jakim formacie chciałby pracować z RTC. Możesz bowiem użyć biblioteki time.h i pracować z czasem UNIXowym lub WinCE.
Oczywiście możesz napisać swoją obsługę RTC, jeśli czujesz się na siłach. Polecam gorąco coś takiego zrobić. Można się wiele nauczyć po drodze 🙂
Możesz też skorzystać z biblioteki od ST, która jest dołączona do generowanego projektu z CubeMX. Działa ona trochę inaczej niż UNIXowy standard. Właśnie tą biblioteką zajmę się w najbliższym czasie.
Podtrzymanie bateryjne
Ciekawym ficzerem wbudowanym w RTC są rejestry backup’owe. W F103 jest ich (aż!) 10. Wszystkie są 16-bitowe, więc można zachować 20 bajtów danych. Wydaje się mało, prawda?
Rejestry te podtrzymywane są bateryjnie, w momencie, gdy zanika napięcie zasilania lub mikrokontroler wchodzi w tryby obniżonego poboru mocy. Z uwagi na specjalne podtrzymanie bateryjne dostęp do nich jest kontrolowany przez specjalny bit.
W normalnym trybie zasilania rejestry te działają z napięcia podanego na VDD. Układ resetu mikrokontrolera wykrywa zanik VDD i przełącza na zasilanie VBAT oprócz rejestrów backup’owych jeszcze kilka szczególnych bloków. Łącznie są to:
- Backup Registers
- RTC – zegar może cały czas chodzić
- LSI – wewnętrzny kwarc do napędzania RTC
- PC13 – Tamper detection
Dzięki temu pełna funkcjonalność RTC jest zachowana po odłączeniu zasilania głównego.
Tamper detection
Jest jeszcze coś takiego jak Tamper detection. Tamper z angielskiego to majstrować. Jest to detekcja majstrowania przy urządzeniu 🙂
Na czym to polega w STM32F103? RTC ma specjalny pin PC13, który jest podtrzymywany bateryjnie, oraz może być skonfigurowany jako RTC_TAMPER. Pin ten może być podłączony do jakiejś krańcówki. Po wykryciu ustawionego zbocza na tym pinie(na przykłąd, gdy ktoś otworzy obudowę), wywoływany jest Event, który kasuje całą zawartość Backup Registers.
Co ważne, event Tamper działa również z zasilania bateryjnego.
Szczerze mówiąc jeszcze nigdy nie użyłem tego mechanizmu. Jeśli masz jakieś ciekawe zastosowanie i prawdziwy przykłąd użycia, to opisz to w komentarzu.
Alarm RTC
Nasz RTC wbudowany w STM32F103 ma prosty alarm. Istnieje specjalny 32-bitowy rejestr przeznaczony do tego celu. Do niego wpisujesz wartość budzika i jeżeli wartość z głównego licznika RTC zrówna się z wartością alarmu, wygenerowane zostanie przerwanie.
Platforma testowa
Jak wspomniałem wcześniej, działanie RTC przedstawiał Ci będę na znanej i lubianej płytce BluePill z STM32F103C8T6. To aktualnie moja jedyna platforma z STM32F1, którą posiadam.
Oprogramowanie, którego użyję to:
- STM32CubeIDE v1.2.0
- STM32CubeMX v 5.5.0 wbudowany w IDE
- HAL F1 v 1.8.0
Konfiguracja w CubeMX
Do celów testowych, oprócz RTC potrzebuję jeszcze kilka innych rzeczy.
Najpierw zegary:
- HCLK ustawiłem na maksymalną wartość 72 MHz korzystając z zewnętrznego oscylatora HSE.
- Jako zegar dla RTC na początek wybierz LSI – później pokażę Ci jak przejść na LSE
Do celów debugowych użyłem:
- UART2 w konfiguracji 115200 8n1
- Serial Wire w zakładce System Core > SYS > Debug > Serial Wire
- Wejście GPIO PA10 oznaczone jako TEST
Teraz można skonfigurować RTC. Znajduje się on w drzewku z Timerami.
Zaznacz ptaszek Activate Clock Source – do włączenia RTC oraz podania źródła taktowania. Aktywuj również kalendarz drugim ptaszkiem. Zaraz, zaraz! Mówiłem, że nie ma tutaj kalendarza, co nie? O co chodzi?
Będzie to programowy kalendarz dostarczany przez bibliotekę od ST 🙂
Poniżej masz ustawienia RTC. Możesz ustawić fabryczną godzinę oraz datę. Rok zapisujesz w zakresie 0-99, bez setek i tysięcy.
Przełącz jeszcze Data Format na binarny. Nie trzeba będzie później przeliczać z BCD na “ludzkie” liczby i odwrotnie.
W polach pod General zostaw wszystko standardowo, na automatyczne liczenie dzielników(wygoda) i Alarm na pinie TAMPER(nie ma to znaczenia w tej chwili dla nas).
Możesz wygenerować projekt 🙂
Kod RTC
Dostaliśmy do pracy bibliotekę od ST, w której zaimplementowany jest też kalendarz programowy. Do operacji na RTC potrzebujesz utworzyć dwie instancje struktur – czasu i daty. Pozwól, że pominę elementy dodatkowe związane z przesyłaniem danych na UART.
RTC_TimeTypeDef RtcTime; RTC_DateTypeDef RtcDate;
Każda z tych struktur posiada pola odpowiadające za każdy element daty i czasu.
typedef struct { uint8_t Hours; /*!< Specifies the RTC Time Hour. This parameter must be a number between Min_Data = 0 and Max_Data = 23 */ uint8_t Minutes; /*!< Specifies the RTC Time Minutes. This parameter must be a number between Min_Data = 0 and Max_Data = 59 */ uint8_t Seconds; /*!< Specifies the RTC Time Seconds. This parameter must be a number between Min_Data = 0 and Max_Data = 59 */ } RTC_TimeTypeDef; typedef struct { uint8_t WeekDay; /*!< Specifies the RTC Date WeekDay (not necessary for HAL_RTC_SetDate). This parameter can be a value of @ref RTC_WeekDay_Definitions */ uint8_t Month; /*!< Specifies the RTC Date Month (in BCD format). This parameter can be a value of @ref RTC_Month_Date_Definitions */ uint8_t Date; /*!< Specifies the RTC Date. This parameter must be a number between Min_Data = 1 and Max_Data = 31 */ uint8_t Year; /*!< Specifies the RTC Date Year. This parameter must be a number between Min_Data = 0 and Max_Data = 99 */ } RTC_DateTypeDef;
Łatwo jest więc odwoływać się do nich. Zauważ, że data również ma numer dnia tygodnia. Nie musisz go ręcznie uzupełniać. Podczas aktualizacji daty jest on wyliczany i wpisywany automatycznie.
Teraz w pętli głównej można łatwo odczytać datę i czas.
HAL_RTC_GetTime(&hrtc, &RtcTime, RTC_FORMAT_BIN); HAL_RTC_GetDate(&hrtc, &RtcDate, RTC_FORMAT_BIN);
I tyle. Wystarczy teraz przesłać to na UART. Ja dodałem jeszcze mały mechanizm polegający na tym, że wysyłam na UART jedynie wtedy, gdy zmieni się sekunda. Nie potrzebujemy przecież lawiny danych na porcie szeregowym.
if(RtcTime.Seconds != CompareSeconds) { MessageLen = sprintf((char*)Message, "Date: %02d.%02d.20%02d Time: %02d:%02d:%02d\n\r", RtcDate.Date, RtcDate.Month, RtcDate.Year, RtcTime.Hours, RtcTime.Minutes, RtcTime.Seconds); HAL_UART_Transmit(&huart2, Message, MessageLen, 100); CompareSeconds = RtcTime.Seconds; }
Oto wynik.
Przeładowanie daty i godziny na starcie MCU
Gdy zresetujesz mikrokotroler to zauważysz, że data i godzina wróciła do tych ustawionych z Cube’a. W Internecie możesz znaleźć niezliczoną ilość wątków poświęconych temu strasznemu “bugowi”.
Nie jestem pewien, czy to w ogóle można nazwać bugiem. Jest to normalne zachowanie biblioteki. To tak jakbyś napisał w mainie przed pętlą główną, że chcesz ustawić jakąś datę i oczekiwał, że się nie zmieni między resetami. Będzie tak samo.
Jak z tym zawalczyć? Przecież to jest generowane przy pomocy Cube’a. Wykasowanie linijek ustawiających godzinę i datę nic nie da, gdy przegenerujesz kod w Cube. Przy potrzebie przegenerowania kodu po jakimś czasie gwarantuję Ci, że zapomnisz o tym i będziesz szukał problemu od nowa. Tak nie robimy!
Jeśli spojrzysz w kod inicjalizacyjny RTC w pliku rtc.c, zauważysz, że tuż przed ustawieniem daty i godziny jest specjalna sekcja użytkownika. To właśnie z niej skorzystasz.
Wstaw tutaj zwykły return. Nie pozwoli on dotrzeć mikrokontrolerowi do momentu nastawy RTC, więc czas będzie biegł taki, jak przed resetem. Pięknie!
void MX_RTC_Init(void) { RTC_TimeTypeDef sTime = {0}; RTC_DateTypeDef DateToUpdate = {0}; /** Initialize RTC Only */ hrtc.Instance = RTC; hrtc.Init.AsynchPrediv = RTC_AUTO_1_SECOND; hrtc.Init.OutPut = RTC_OUTPUTSOURCE_ALARM; if (HAL_RTC_Init(&hrtc) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN Check_RTC_BKUP */ // // You have to do not let Init code to reinitialize Time and Date in RTC. // It's "a bug" in HAL widely described and complain on forums. // That causes every MCU restart the time and date will be same as you configured in CubeMX. // All you have to do is just return before init time/date in this user section. // return; /* USER CODE END Check_RTC_BKUP */ /** Initialize RTC and set the Time and Date */ sTime.Hours = 23; sTime.Minutes = 59; sTime.Seconds = 55; if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK) { Error_Handler(); } DateToUpdate.WeekDay = RTC_WEEKDAY_SATURDAY; DateToUpdate.Month = RTC_MONTH_FEBRUARY; DateToUpdate.Date = 3; DateToUpdate.Year = 20; if (HAL_RTC_SetDate(&hrtc, &DateToUpdate, RTC_FORMAT_BIN) != HAL_OK) { Error_Handler(); } }
Teraz po resecie godzina jest pamiętana za to data nie…
Problemy z datą
Data nie jest pamiętana, ponieważ nie jest ona składową sprzętowego RTC! Dodatkowo biblioteka od ST nie traktuje sprzętowego licznika RTC jako data + czas. Jest on użyty jedynie do zliczania sekund, a gdy licznik przekręci się – dzień jest wyzerowany.
Struktura daty jest to zwykła zmienna w SRAM, którą przechowuje biblioteka. Normalne jest więc, że po resecie mikrokontrolera będzie wyzerowana!
Da się z tym powalczyć. Pamiętasz Backup Registers? Właśnie nimi.
Jednak tym zajmę się następnym razem, bo ten wpis wyszedł już całkiem spory.
Podsumowanie
Jak widzisz, z RTC nie skorzystasz od ręki tak, jak z tych w układach scalonych. Mimo że konfiguracja w Cube oraz używanie jest dosyć łatwe, tak pojawił się problem ze znikającą datą oraz brakiem jej potrzymania przy braku zasilania.
Jednak nie martw się! Już w kolejnym artykule pokażę Ci jak sobie z tym poradzić. Zajmę się też przełączeniem na pracę z zewnętrznym kwarcem, bo to też przyniesie pewną niespodziankę 😉
Pełny projekt wraz z biblioteką znajdziesz jak zwykle na moim GitHubie: LINK
Jeśli zauważyłeś jakiś błąd, nie zgadzasz się z czymś, chciałbyś coś dodać istotnego lub po prostu uważasz, że chciałbyś podyskutować na ten temat, napisz komentarz. Pamiętaj, że dyskusja ma być kulturalna i zgodna z zasadami języka polskiego.
7 komentarzy
Jose Fernando Sakugava · 27/02/2021 o 12:09
Hi. Is it possible to use the LED on PC13 when RTC is activated?
dambo · 07/02/2020 o 22:12
To wpis “Dlaczego nie robię wpisów na podstawie BluePill” do wywalenia?
No ale RTC zawsze spoko – to kiedy zobaczymy projekt zegarka?
Mateusz Salamon · 08/02/2020 o 14:44
Ha! Masz mnie 😛 Niestety zostanie… Jednak jak coś muszę zrobić na F1, to nie mam w tej chwili nic innego niż BluePill 🙁
Na razie nie planuję robić zegara chyba, że odnowię swój z początków nauki elektroniki na lampie VFD IW-18. Lubię ten klimat lamp 🙂
Optimex · 07/02/2020 o 12:32
Apropo modułu RTC i pinów z nim związanych warto tu dodać, że są one zasilane z pinu VB i MUSI on być podłączony do zasilania/baterii (nota) jeśli korzysta się z pinów c14 i c15 jako GPIO nawet jeśli nie korzysta się z zasilania podtrzymującego. Ze względu na to, że są to piny od rezonatora RTC i ich niską wydajność prądową uważam, że powinny być zarezerwowane tylko pod RTC i niepotrzebnie zostały wyprowadzone na zewnątrz płytki, najlepiej nie lutować do nich wyprowadzeń.
Mateusz Salamon · 07/02/2020 o 12:41
Dobrze gadasz! Zgadzam się z tym, że niepotrzebnie są wyprowadzone. Zwłaszcza, że w BluePillu VBAT nie jest domyślnie zasilane.
W ogóle z tymi pinami od rezonatora RTC które są wyprowadzone na zewnątrz wiąże się jeszcze inna przykra sprawa… ale o tym w kolejnym wpisie 😀
Adam · 07/02/2020 o 11:43
Ciekawy artykuł ale do końca nie rozwiązuje problemu nie incrementowania daty. Jeśli osoba dociekliwa zerknie do źródeł tak bardzo nielubianej biblioteki SPL to okaże się iż tam wszystko działa jak powinno. Dla własnych potrzeb używam dwóch funkcji które doskonale się sprawdzają.
uint32_t RTC_GetCounter(void){
uint16_t high1 = 0, high2 = 0, low = 0;
high1 = RTC->CNTH;
low = RTC->CNTL;
high2 = RTC->CNTH;
if (high1 != high2)
{ /* In this case the counter roll over during reading of CNTL and CNTH registers,
read again CNTL register then return the counter value */
return (((uint32_t) high2 <CNTL);
}
else
{ /* No counter roll over during reading of CNTL and CNTH registers, counter
value is equal to first value of CNTL and CNTH */
return (((uint32_t) high1 <tm_year -= 100;
stimestructure->Hours = time_struct->tm_hour;
stimestructure->Minutes = time_struct->tm_min;
stimestructure->Seconds = time_struct->tm_sec;
sdatestructure->Year = time_struct->tm_year;
sdatestructure->Month = time_struct->tm_mon;
sdatestructure->Date = time_struct->tm_mday;
sdatestructure->WeekDay = time_struct->tm_wday;
}
Gdzie użycie RTC już nie jest takie straszne i dostęp do daty uzyskujemy poprzez zwykły odczyt struktury.
sprintf(buf,” %s, %d %s 20%02d “, ndzien[sdatestructure.WeekDay], sdatestructure.Date, nmies[sdatestructure.Month], sdatestructure.Year);
Mateusz Salamon · 07/02/2020 o 12:02
Hej! To jest pierwsza część i o date będzie w kolejnej 🙂
Jak dokładnie wygląda problem z inkrementacją daty? U mnie chodzi już kilka dni i wszystko się zgadza 😉