fbpx

W poprzednim wpisie rozpocząłem mini cykl poświęcony wbudowanemu RTC w układy STM32. Zacząłem od chyba najpopularniejszego mikrokontrolera SMT32F103 znajdującego się między innymi w taniej płytce BluePill. Dotarłem do momentu, w którym po resecie mikrokontrolera godzina nadal była prawidłowa natomiast data startowała od zera.

Dlaczego data jest zerowana?

No dobra przecież tylko resetuję MCU, więc licznik RTC biegnie dalej. Dlaczego więc godzina po resecie zgadza się natomiast data już nie?

Bierze się to z tego, w jaki sposób ST korzysta ze wbudowanego licznika RTC. Przypomnę Ci, że jest on wykorzystywany w ten sposób, że jak przekręci się dzień, to licznik jest zerowany. Po prostu nie przetrzymuje on kompletnej daty jak ma to miejsce w Linuksie.

Stąd właśnie po resecie godzina jest zachowywana. Data trzymana jest w zwykłej zmiennej w pamięci RAM, która resetowana jest po uruchomieniu mikrokontrolera. I to jest właśnie przyczyna “problemu”. Jak z tym zawalczyć?

Użyjmy w końcu Backup Registers!

Kilka razy już wspominałem o rejestrach backupowych. Przypomnę Ci, że jest to kilkanaście/kilkadziesiąt rejestrów podtrzymywanych bateryjnie. Dodatkowo zerowanie tych rejestrów wymaga specjalnej procedury, która nie jest zawarta w zwykłym resecie MCU.

Zapis do tych rejestrów również wymaga odblokowania. W HALu, blokada ta jest domyślnie zdjęta. Wykonywane jest to w funkcji HAL_RTC_MspInit

HAL_PWR_EnableBkUpAccess();

Skoro mamy zdjętą blokadę, możemy pisać do tych rejestrów. Robi się to bardzo prosto. Wystarczy użyć funkcji HALowskiej z rozszerzonej palety funkcji RTC.

void HAL_RTCEx_BKUPWrite(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister, uint32_t Data);

Podajesz w niej uchwyt do RTC, numer rejestru backupowego oraz daną. Niestety tutaj jest mylący argument, ponieważ wskazuje na 32-bitową zmienną którą możesz wsadzić do rejestru. BKP mieszą TYLKO 16 bitów.

Swoją drogą te wszędobylskie 32-bitowe argumenty są plagą HALa, którą wykorzystują przeciwnicy do wszczynania zadym na forach elektronicznych. Po części się z nimi zgadzam, ale bez przesady, że to eliminuje totalnie HALa 🙂

Ok więc jak wpisać aktualną datę do tych rejestrów? Skoro mieszczą po 2 bajty, a każda ze zmiennych daty mieści się w jednym bajcie to wystarczą dwa rejestry backupowe, aby zamknąć w nich kompletną datę. Zrobiłem w mainie taką prostą funkcję.

void BackupDateToBR(void)
{
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR2, ((RtcDate.Date << 8) | (RtcDate.Month)));
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR3, ((RtcDate.Year << 8) | (RtcDate.WeekDay)));
}

Wpisuję do rejestru BKP_DR2 dzień i miesiąc, a do BKP_DR3 Rok i dzień tygodnia. Dzień tygodnia nie jest obowiązkowy – jest on zawsze wyliczany w bibliotece od ST na nowo.

Kiedy robić backup daty? Nie ma sensu robić tego w każdej sekundzie. Wystarczy tylko wtedy, gdy zmieni się data.

if(RtcDate.Date != CompareDate)
{
    BackupDateToBR();
    CompareDate = RtcDate.Date;
}

Odzysk daty na starcie

Teraz wystarczy odczytać te dane na starcie i nastawić ją przy pomocy znanej już nam funkcji HAL_RTC_SetDate. Spróbujmy!

W sekcji /* USER CODE BEGIN Check_RTC_BKUP */ w inizjalizacji RTC, w której ostatnio wstawiłem return, aby nie ustawiać zegara danymi wprowadzonymi w Cube dopiszę trochę kodu. Odczytam z BKP Regs zapisaną datę i ustawię kalendarz.

/* USER CODE BEGIN Check_RTC_BKUP */
RtcDate.Date = (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2) >> 8);
RtcDate.Month = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2);
RtcDate.Year = (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR3) >> 8);
RtcDate.WeekDay = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR3);

HAL_RTC_SetDate(&hrtc, &RtcDate, RTC_FORMAT_BIN);

//
//  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 */

Taki kod spowoduje, że po resecie mikrokontrolera data będzie pamiętana. Sukces! …do czasu…

Przeskok daty a backup

Co się stanie, jeśli w trakcie zasilania bateryjnego przeskoczy data? O jeden, dwa lub 20 dni? Zanim przejdę do zasilania bateryjnego sprawdźmy to symulując taką sytuację w debuggerze. Jak? Podmieniając wartości w liczniku RTC symulując przeskok daty pod “nieobecność” rdzenia.

W celu podmiary rejestru uruchom debug i zpauzuj wykonywanie kodu. Teraz w prawym górnym rogu masz kilka sekcji. Wejdź w SFRs. Są to Special Function Registers, czyli upraszczając to można powiedzieć, że są to rejestry peryferiów. Rozwiń drzewko dla RTC.

Interesują nas trzy rejestry – CRL, CNTH i CNTL.

CRL – jest to rejest kontrolny dla RTC. Będzie on nam potrzebny po to, aby RTC pozwoliło nam wpisać swoją wartość do licznika. Służy do tego bit nr 4, czyli CNF.

CNTH i CNTL – licznik RTC górny i dolny. Obydwa 16-bitowe. To tutaj zliczane są sekundy przez RTC.

Teraz, aby podmienić wartość w liczniku potrzebujesz:

  1. Ustaw bit CNF rejestru CRL na 0x1
  2. Podmień wartości CNTH i CNTL – w Eclipse “wrócą” do swoich wartości po wpisaniu
  3. Skasuj bit CNF rejestru CRL (0x0), aby zatrzasnąć wprowadzone zmiany w liczniku.

O ile zwiększyć ten licznik RTC? Chcemy sprawdzić zmiany o dzień lub kilka. Jeden dzień to 86400 sekund, więc tyle dla każdego dnia musisz dodać do licznika, aby zasymulować zmianę dnia.

Dodam 2 dni i kilka sekund – ustawię licznik na 180000. CNTH = 0x2, CNTL = 0xBF20. 

Przed zmianą podczas pauzy mam ustawioną datę 17.05.2020 i godzinę 16:58:17(poleciałem w przyszłość). Teraz podmienię licznik i zresetuję MCU. To jest właśnie pojawienie się zasilania MCU przy podtrzymaniu bateryjnym na RTC.

Co się stało?

Kurde coś jest nie tak! Godzina się zmieniła, ale dzień nie został zaktualizowany. Jak to? Przecież pamiętaliśmy datę w rejestrze backupowym.

Tutaj właśnie wychodzi ułomność rozwiązania od ST… Problem tkwi w fukncji HAL_RTC_SetDate. Wewnątrz tej funkcji dokonywana jest korekta licznika RTC, która w skrócie polega na OBCIĘCIU UPŁYNIĘTYCH DNI. Powoduje to, że ilość upłyniętych dni znika z licznika RTC i nie jest w żaden sposó przenoszona na datę. SKANDAL!

To jest straszny problem, bo mimo użycia rejestrów backupowych nadal nie jesteśmy w stanie w łatwy sposób zapanować nad datą. Są dwa rozwiązania:

  1. Napisać swoją obsługę RTC, która będzie lepiej przemyślana.
  2. Zrobić trochę gimnastyki kodem i odzyskać datę bez modyfikacji kodu od ST(HALa i libki RTC).

Pozwól, że zrobię punkt drugi. Tak dla treningu oraz pokazania, że da się jednak skorzystać z biblioteki, którą generuje Cube.

Pełne odzyskiwanie daty

Oj przeciwnicy HALa będą mnie nienawidzieć, bo zrobię teraz workaround polegający na dodaniu wielu cykli zegara. Może jednak pochwalą mnie za pomysłowość 🙂 Zapraszam do dyskusji.

Będę potrzebował dwóch zmiennych RTC_DateTypeDef:

  1. RtcDate, której używam wszędzie w późniejszym kodzie. To jest zmienna docelowa, w której ma być data.
  2. BackupDate – zmienna tymczasowa do obliczeń.

Wszystko będzie się działo w inicjalizacji RTC. Po starcie MCU zbieram rejestry backupowe do zmiennej docelowej.

/* USER CODE BEGIN Check_RTC_BKUP */

RTC_DateTypeDef BackupDate;

RtcDate.Date = (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2) >> 8);
RtcDate.Month = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR2);
RtcDate.Year = (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR3) >> 8);
RtcDate.WeekDay = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR3);

W następnym kroku zbieram czas, który jest prawidłowy do docelowej zmiennej czasu. W tym momencie obliczana jest jednocześnie nowa data na podstawie licznika RTC. Dodając 2 dni, będzie to 3 stycznia 00 roku, bo kalendarz software’owy od ST liczy od 1 stycznia 00 roku.

Teraz zbieram tę datę do tymczasowej zmiennej backupowej.

HAL_RTC_GetTime(&hrtc, &RtcTime, RTC_FORMAT_BIN); // There is also an internal date update based on HW RTC time elapsed!!
HAL_RTC_GetDate(&hrtc, &BackupDate, RTC_FORMAT_BIN); // Days elapsed since MCU power down

Teraz wystarczy dodać te 2 dni do naszej docelowej zmiennej kalendarzowej i wszystko gitara? Tak! Tylko… co jeśli będzie to tyle dni, że w nowej dacie przeskoczy miesiąc czy rok. Obliczenia robią się już skomplikowane…

Operacje na liczbie upłyniętych dni

Znalazłem dwa przydatne algorytmy działające na liczbach całkowitych. Mają ona jednak pewne problemy przy roku zerowym, ale to jest do obejścia.

Pierwsza z funkcji CalculateDayNumber ma za zadanie zwrócenie numeru dnia od początku kalendarza na podstawie daty.

Druga zwraca datę na podstawie numeru dnia.

W celu ominięcia dziwnych błędów przy roku zerowym dodałem i odjąłem 20 lat w obydwu funkcjach. 20 dlatego, że tak samo, jak rok zerowy, jest to rok przestępny. Nie będzie więc błędów z tego względu.

/* USER CODE BEGIN 1 */
uint32_t CalculateDayNumber(uint8_t Date, uint8_t Month, uint8_t Year)
{
	// Format:
	// DD.MM.YY
	uint32_t _Year = Year + 20;
	Month = (Month + 9) % 12;
	_Year = _Year - (Month / 10);
	return ((365 * _Year) + (_Year / 4) - (_Year / 100) + (_Year / 400) + (((Month * 306) + 5) / 10) + (Date - 1));
}

void CalculateDateFromDayNumber(uint32_t DayNumber, uint8_t *Date, uint8_t *Month, uint8_t *Year)
{
	uint32_t _Date, _Month, _Year;
	_Year = ((10000 * DayNumber) + 14780) / 3652425;
	int32_t ddd = DayNumber - ((365 * _Year) + (_Year / 4) - (_Year / 100) + (_Year / 400));
	if (ddd < 0)
	{
		_Year -= 1;
		ddd = DayNumber - ((365 * _Year) + (_Year / 4) - (_Year / 100) + (_Year / 400));
	}
	int32_t mi = ((100 * ddd) + 52) / 3060;
	_Month = (mi + 2) % 12 + 1;
	_Year = _Year + (mi + 2)/12;
	_Date = ddd - ((mi * 306) + 5)/10 + 1;
	*Date = _Date;
	*Month = _Month;
	*Year = _Year - 20;
}
/* USER CODE END 1 */

Teraz mając backupową datę oraz datę, która minęła podczas nieobecności zasilania mogę obliczyć odpowienio ich numery dnia.

  uint32_t BackupDateDays = CalculateDayNumber(BackupDate.Date, BackupDate.Month, BackupDate.Year);
  uint32_t RtcDateDays = CalculateDayNumber(RtcDate.Date, RtcDate.Month, RtcDate.Year);

Do docelowej daty wystarczy dodać dni, które minęły. Jednak trzeba odjąć pierwszego stycznia z uwagi na to, że wewnątrz algorytmu tak na prawdę liczymy dzień 20 lat wprzód(z uwagi na błędy dla roku 0000).

RtcDateDays += (BackupDateDays - CalculateDayNumber(1, 1, 0));

Teraz jedyne co trzeba to numer dnia przekształcić na datę przy pomocy drugiego algorytmu.

CalculateDateFromDayNumber(RtcDateDays, &RtcDate.Date, &RtcDate.Month, &RtcDate.Year);

Na koniec wpisać tę datę do kalendarza software’owego.

HAL_RTC_SetDate(&hrtc, &RtcDate, RTC_FORMAT_BIN);

To tyle 🙂

Teraz data zawsze(w 99%) będzie prawidłowa. Przetestuję ponownie wpisując 180000 do RTC.

Czy to działa zawsze? Nie dam sobie ręki uciąć. Aaaaale spróbujmy dodać… 700 dni, co?

19.05.20 + 700 dni = 19.04.22 według kalkulatora.

700 dni * 86400 sekund = 60480000 sekund (0x39ADA00). Plus 2 godziny daje 60847200 (0x39AF629) i tą wartość wpiszę do RTC.

I co? Działa!

Podsumowanie

Użycie biblioteki od ST to nie lada wyzwanie. Twórcy naszych ulubionych STMów zrobili nam niezłego psikusa dostarczając tak ciężką w użytkowaniu bibliotekę. Jednak wbrew temu, co mówią najmądrzejsi – da się jej używać.

Pamiętaj też, że tak jest dla najbiedniejszego RTC, w którym jest jedynie licznik sekund. W STM32F4 jest już sprzętowy kalendarz. Na pewno go przetestuję w kolejnych wpisach.

Artykuł znowu mi się rozrósł. Pozostały jeszcze dwie kwestie. Podtrzymanie bateryjne, które nie jest tak banalne, jak można się spodziewać oraz napędzanie RTC zewnętrznym kwarcem, który na BluePill też sprawi kilka problemów. To już w kolejnym wpisie!

Tymczasem zapraszam do sekcji komentarzy. Daj znać czy podobał Ci się ten wpis i czy jest sens robić takie artykuły. Każdy komentarz jest dla mnie cenny.

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.

5/5 - (5 votes)

Podobne artykuły

.

21 komentarzy

madasz · 02/04/2022 o 23:47

A co z funkcją RTC_DateUpdate(RTC_HandleTypeDef *hrtc, uint32_t DayElapsed)?
HAL dostarcza świetnego gotowca. Pamiętasz w rejestrach datę bieżącą. Po zaniku i powrocie zasilania z licznika wyliczasz ile dni minęło w trakcie “dżemki”. Tą ilość dni podajesz w parametrze DayElapsed.
Dziękuję…wyliczona data 🙂

    Mateusz Salamon · 03/04/2022 o 14:40

    Ta funkcja nie jest do użytku przez programistę 😉 Jest wewnętrzna dla drivera HALowego RTC i na dodatek statyczna.

      madasz · 03/04/2022 o 21:23

      Tiaaa… Ale wciąż niedowierzałem, że ktoś pisał HALa i tyle różnych funkcji, podfunkcji itd… i żeby nie dało się łatwo ogarnąć obsługi nie tylko czasu ale i kalendarza?? Przyjrzałem się bliżej tym funkcjom i oto sposób na ich wykorzystanie:
      Po powrocie POWER’a odczytujesz datę z rejestrów BKP i wrzucasz je bezpośrednio do struktury uchwytu :
      hrtc.DateToUpdate.Year , hrtc.DateToUpdate.Month oraz hrtc.DateToUpdate.Date – dzień tygodnia wylicza się sam.
      I teraz trochę nabierasz HALa, każąc mu odczytać datę !! Nie zapisać, tylko odczytać! 🙂
      Funkcja HAL_RTC_GetDate odczytując datę, sprawdza też timer czasu i jeśli naklepał sobie na baterii więcej niż 24 godziny, to wywołuje RTC_DateUpdate(…) tym samym dodając te zliczone dni do wpisanej przed chwilą daty z BKP rejestrów do hrtc.DateToUpdate…
      HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
      Działa! Sprawdziłem! I wykorzystujesz samego HALa bez kombinacji, czyli nie taki ten HAL beznadziejny… tylko dokumentację ma słabiutką 😉
      Pozdrawiam!

      Mateusz Salamon · 04/04/2022 o 09:35

      No i jest sposób 🙂 Fajnie. Tylko, czy wciąż musisz wycinać w inicjalizacji nastawienie daty? Pewnie tak 🙂 Może zaktualizowałbym wpis o Twoją metodę… 😉

      madasz · 04/04/2022 o 14:48

      Spoko.
      A co do wycinki w init – bez zmian.

Florin · 08/07/2021 o 12:18

Hi, I have tested with STM32F105, but sometimes the date doesn’t change after power on. It remain the same with the last when the micro was power off. Any ideas?

    Mateusz Salamon · 14/07/2021 o 10:34

    Any code or schematic?

Roman · 22/05/2020 o 08:36

Zawsze chciałem ruszyć RTC w STM32F103. Jest wiele poradników, ale twój jest “the best”.

    Mateusz Salamon · 28/05/2020 o 12:36

    Dzięki! 🙂

Jacek · 20/02/2020 o 17:47

W czasie jaki należy poświecić na walkę z biblioteka, udałoby się napisać obsługę daty i czasu. To tylko około 50 linijek kodu, wraz ze zmiana czasu lato-zima. No i raz napisane jest na wszystkie uC. 🙂

    Mateusz Salamon · 21/02/2020 o 09:04

    Tylko 50 linijek? No może 100 🙂 Zależy na przykład czy potrzebujesz liczyć dzień tygodnia i jak traktować ten 32-bit licznik. Najłatwiej chyba potraktować go linuksowo – razem z datą. Tak mi się przynajmniej wydaje.

      Jacek · 24/02/2020 o 09:37

      Nie mam przed oczami swoich kodow ale chyba bliżej 50. Przed laty popełniłem kalendarz. Wykorzystuje takt sekundowy i na bierząco, co sekundę aktualizuje, jeśli trzeba, zmienne day, min, hour, day, day of week, month, year. zmiany czasu lato zima to około 12 linijek. No i na potrzeba ustawiania czasu ze dwie funkcje do obliczania aktualnego dnia tygodnia i czy jest zima czy lato. Ale sprawdze za pare dni i policzę linijki bo pisałem to w 1996 🙂

Jacek · 20/02/2020 o 17:01

No, niezłe kombinacje. W tym czasie byś napisał własna obsługę czasu i daty :). Wraz ze zmiana czasu lato-zima to chyba w 50 linijkach się mieści. A i na przyszłość się przyda, do innych procesorów.

    Jacek · 24/02/2020 o 16:34

    Cos słabo mi działa dodawanie tu komentarzy 🙁

Optimex · 20/02/2020 o 15:44

Chyba nie chciałbym uczyć się na pamięć jak omijać błędy HALa. Lepiej byłoby zastosować jednak sposób pierwszy i napisać samemu obliczanie daty korzystając z rejestrów bckup.
Nie rozumiem dlaczego ST nie naprawił tego błędu przez tyle lat, uważam, że wbudowany RTC powinien być opisany fabrycznie w taki sposób, żeby można go było łatwo używać za pomocą 3 funkcji i się nad nim nie zastanawiać ani przez chwileczkę.

F1 jest już trochę przestarzały ale może warto to zgłosić producentowi jednocześnie proponując rozwiązanie, a nóż wynagrodzi starania 😉

    Mateusz Salamon · 21/02/2020 o 09:02

    Ja po części rozumiem ST dlaczego nie piszą lepszej, poprawnej obsługi. Bo i tak każdy w embedded “musi” zrobić swoją bibliotekę. Nie ważne jak dobre byłyby dostarczane te przez producenta. Pracując wcześniej jako FAE właśnie to zauważyłem 🙂 Często męczyłem się pisać koda dla wyświetlacza tak, aby klient podpiął jedynie jakiś “send byte” a on i tak przepisywał wszystko na nowo. W efekcie później wymienialiśmy masę maili “bo coś u mnie nie działa”…

    Z tym RTC byłoby to samo. Gdyby ST dostarczyło super bibliotekę to i tak byłoby “fajnie, że działa, ale i tak napiszę swoją” 🙂

      Jacek · 24/02/2020 o 16:40

      Gdyby producenci pisali poprawnie działające to by każdy korzystał. Ale nie robią tego bo ich celem jest tylko zachęcenie programistów do zastosowania danej platformy. A potem męczcie się.

      Przemek · 03/03/2020 o 23:58

      Nie do końca się zgadzam z uwagą że “każdy i tak napisze własną bibliotekę”. Może to dotyczy osób które tworzą komercyjne układy. Jest jednak wielu pasjonatów którzy robią sobie układy na własne potrzeby. Takie osoby (jak ja) poszukują gotowych układów peryferyjnych i bibliotek aby użyć kilku zaklęć do ich obsługi np “odczytaj aktualną datę/czas”, “ustaw datę/czas”. Nie interesuje mnie cały proces konfiguracji itd. Jak wiem że następuje np reset daty to oczywiście zapis i odczyt daty z EEPROM’u (Arduino) czy backup rejestru (STM32) jest wystarczającym dodatkiem (w tym przypadku koniecznym).

      Mateusz Salamon · 10/03/2020 o 09:47

      Masz 100% racji. Chyba ostatnio za dużo osób bardziej zaawansowanych do mnie pisało, które przekreślały totalnie rozwiązanie RTC od ST 🙂 Ja również jestem za tym, że jak ktoś nie potrzebuje się wdrożyć, albo chce zrobić coś na szybko (PoC) nie mając zbytnio wiedzy o samym RTC w STM32, to skorzystanie z libki jest najlepsze.

Damian G. · 20/02/2020 o 12:20

Skoro operujesz na roku czterocyfrowym, czyli nie potrzebujesz zapisać roku 2000 jako roku 0, to dlaczego walczysz z problemem roku 0000, który w rzeczywistości nie istniał?
Bo przed rokiem 1 n.e był rok 1 p.n.e lub -1 jak kto woli i roku 0 nie było (bo starożytni w większości nie znali zera i pożytków z niego płynących).
W tym wypadku rok 0 oznacza, że straciliśmy informację o roku i pewnie całej dacie, więc i tak sens dalszych obliczeń jest nikły.

    Mateusz Salamon · 20/02/2020 o 12:31

    Nie operuję na roku 4-cyfrowym. Dopisuję 20 z przodu i działam na dwóch cyfrach według tego, co jest dostarczane przez libkę od ST. Walczę z zerem bo krzem w STMie nie wiedział o starożytnych i braku roku zerowego i STM + libka domyślnie mają rok 00, który nie istnieje. Algorytm obliczeń pewnie już wiedział o tym i dlatego wszystkie obliczenia dla roku nr 00 są błędne.
    Po moich “dodatkach” libka staje się używalna i odporna na regenrowanie kode przez Cube’a.
    Mam świadomość, że można zrobić to o niebo lepiej.

Dodaj komentarz

Avatar placeholder

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *