Biblioteka standardowa time.h i mikrokontrolery

Może ktoś z was będzie miał jakiś pomysł, bo od paru dni nie mogę dojść do tego, co robię nie tak. Sytuacja wygląda następująco.

Środowisko: Płytka NUCLEO-L031K6, programowana za pomocą IDE AtollicSTUDIO. Projekt generowany narzędziem STM32CubeMX.

Software: Biblioteka do obsługi DCF77, przeportowana z Arduino. Do tego biblioteka sterująca wyświetlaczem od Nokii, przeportowana z AVR. Obydwie zdają się działać prawidłowo.

DCF odczytuje w przerwaniu impulsy z modułu i sprawdza poprawność ramek. Kontrolowane jest m.in. to, czy odebrany czas nie różni się za bardzo od systemowego zegara. Jeśli mamy do czynienia z taką sytuacją, konieczne jest odebranie dwóch prawidłowych ramek, jedna po drugiej. Oryginał korzystał w tym celu z jakiejś arduinowej biblioteki, ja przerobiłem to na standardowe time.h, podkładając wszędzie gdzie trzeba wywołania time(NULL).

Oczywiście zdefiniowałem w kodzie swoją własną funkcję time(), która czyta systemowy RTC, wypełnia pobranymi wartościami strukturę struct tm, a następnie przy pomocy mktime() generuje timestamt time_t.

Sam moduł RTC jest obsługiwany za pomocą bibliotek HAL. Po odebraniu nowej ramki zegar jest ustawiany wartościami uzyskanymi za pośrednictwem gmtime().

I teraz przechodząc do sedna sprawy: na początku wszystko zdaje się działać prawidłowo. Po starcie układu zegar zaczyna odliczać w górę, a jego aktualną wartość wyświetla się na LCD. Jeśli pojawią się odpowiednie warunki propagacyjne, DCF zaczyna dobierać ramki i przestawia zegar. Dodatkowo na LCD wyrzucam też info o czasie ostatniej poprawnej synchronizacji. Potrafi to działać prawidłowo przez kilka godzin, aż w końcu coś się wysypuje. Na przykład wczoraj około 20.00 UTC zegar stwierdził, że jest 4.00 UTC następnego dnia. Żeby było ciekawiej, kolejne ramki DCF były nadal odbierane, a na ekranie pojawiała się informacja o udanych synchronizacjach, oznaczonych prawidłowym czasem (!).

Kilka rzeczy nie daje mi spokoju:

- Dokładność tej anomalii - obserwowałem ją kilka razy i OIDP zawsze było to osiem godzin do przodu. Nie chodzi więc o odebranie jakimś cudem błędnej ramki.

- Przygotowana przeze mnie funkcje time() najwyraźniej zwraca cały czas prawidłowego timestampa, bo inaczej kolejne synchronizacje nie dochodziłyby do skutku. Program stwierdziłby rozjechanie się RTC z odbieranym czasem, czekając na dwie poprawne ramki. Wtedy ustawiłby zegar i wszystko wróciłoby do normy. Tak się jednak nie dzieje. Po pojawieniu się anomalia pozostaje na stałe.

W chwili obecnej do pobierania czasu z RTC używam kombinacji time() i gmtime(), a uzyskane wartości ze struktury wyrzucam na ekran. Po udanej synchronizacji odebrany czas z DCF jest zapisywany do zmiennej i również trafia na ekran za pośrednictwem gmtime().

Ktoś ma jakiś pomysł, co mogę robić nie tak? Może time.h w przypadku mikrokontrolerów wymaga jakiegoś przygotowania (poza podstawieniem własnej funkcji time())? W jaki sposób chociażby definiuje się w niej strefę czasową. Pod Linuksem ustawiało się zmienna środowiskową. A na małym mikrokontrolerze?

Reply to
Atlantis
Loading thread data ...

Atlantis snipped-for-privacy@wp.pl napisał(a):

Dlaczego pisałeś własną funkcję time()? Ta funkcja już jest gotowa, a Twoim zadaniem jest napisanie funkcji _gettimeofday(). time() z biblioteki newlib woła _gettimeofday_r() a ta z kolei _gettimeofday(), którą trzeba dostarczyć. Czy monitorowałeś zawartość RTC, np. wypisując zawartość na porcie szeregowym albo przez semihosting? Strefę czasową ustawia się tak samo: setenv("TZ","CET-1CEST,M3.5.0/2,M10.5.0/3",1);

Reply to
Grzegorz Niemirowski

Hmm... Tak zawsze robiłem na PIC32. Tam próba odwołania się do time() bez zdefiniowania własnej wersji tej funkcji powodowała wyświetlenie warninga:

"Linking code with default time() stub. Hint: Write an app-specific implementation."

To rozwiązanie charakterystyczne dla STM32, czy stanowi ogólnie przyjęty standard i mogę się spodziewać, że tak samo będzie się to robiło także w przypadku innych rodzin MCU, a PIC32 jest tutaj jakimś wyjątkiem?

W ogóle istnieje gdzieś jakiś zasób, który tłumaczyłby w jaki sposób spiąć niskopoziomowo standardową bibliotekę C z własnym sprzętem?

Reply to
Atlantis

Atlantis snipped-for-privacy@wp.pl napisał(a):

ARM, nie STM32, dla ścisłości. Kompilator (gcc-arm-none-eabi) nawet nie wie, że jest takie coś jak STM32. Nie wiem jak jest na innych architekturach, z 32-bitowych używam tylko STM32. Ale w każdym razie z kompilatorem (jako programem) dostarczana jest też biblioteka języka C. Popularną biblioteką C dla mikrokontrolerów ARM jest newlib. I ona wymaga własnie _gettimeofday(), inaczej będzie błąd linkowania. Widocznie w bibliotece rozprowadzanej z kompilatorem dla PIC32 to jest uproszczone, nie ma dodatkowych warstw w postaci _gettimeofday_r() i _gettimeofday() i trzeba od razu napisać time().

Ogólnie jest tak, że w przypadku standardowych funkcji C komunikujących się z tym, co jest na zewnątrz aplikacji (czas, pliki, standardowe we/wy, dynamiczna alokacja pamięci), funkcje te wywołują funkcje systemu operacyjnego (system calls - syscalls). Ponieważ na mikrokontrolerze nie ma systemu operacyjnego, trzeba właśnie te funkcje systemowe napisać samemu. Dlatego jeśli chodzi o ARM GCC, możesz napotkać na posty ludzi, którym linker wywala brak funkcji _write(), _read(), czy _sbrk(). Więc odpowiadając na Twoje pytanie, to nie znam takiego jednego zasobu, ale googlałbym po słowach kluczowych newlib syscalls arm. Można znaleźć np.

formatting link
ładowy plik syscalli z minimalnymi funkcjami:
formatting link
ęki temu, że piszesz własną implementację syscalli możesz np. przekierować printf() (bo on woła pod spodem _write()) na port szeregowy albo konsolę semihostingu: int _write(int file, char *ptr, int len) { if (CoreDebug->DHCSR & CoreDebug_DHCSR_C_DEBUGEN_Msk) { for (int DataIdx = 0; DataIdx < len; DataIdx++) { SH_SendChar(*ptr++); } } return len; }

Reply to
Grzegorz Niemirowski

Czyli rozumiem, że inne ARM-y programuje się w podobny sposób, korzystając z tego samego kompilatora? Na przykład takie AT91SAM7* będą miały to rozwiązane w ten sam sposób, a różnice będą wynikały głównie z nieco innego zestawu peryferiów (np. brak SysTick) oraz innych bibliotek do ich obsługi?

Tak z ciekawości. Jak to wyglądało w przypadku AVR-ów? W czasach, gdy uczyłem się tych mikrokontrolerów nigdy nie musiałem podpinać niskopoziomowych funkcji do obsługi biblioteki standardowej. Zamiast printf używałem zestawu snprintf + uart_puts, a operacje związane z czasem załatwiałem za pomocą zestawu własnych funkcji. Jestem jednak ciekaw na ile dałoby się to zrobić w teorii. Zwłaszcza, że przecież niektóre z wyższych modeli Atmegi i AtXmegi miały już dość sporo pamięci...

Reply to
Atlantis

Nie wiem czy wyjątkiem, libc w pic32 jest niekompletne i zawiera błędy (przynajmniej w tym co było dostępne przed Harmony). Nie da się zaplementowac poprawnie stdio mimo, że jest przygotowane nagłówkowo by to zaimplementować przez własne prymitywy _fopen() _write(), _read() (mimo wlasnej implementacji powyższych fopen() nie działalo prawidłowo, nie pamiętam w czym był problem). Nie zawsze działa prawidłowo sscanf(). To tak w skrócie.

Reply to
Marek

Atlantis snipped-for-privacy@wp.pl napisał(a):

Tak, zgadza się. W obrębie danego kompilatora wszystko będzie odbywało się bardzo podobnie. Standardowe biblioteki C nie zajmują się peryferiami, nie są aż tak niskopoziomowe i daje to pewną przenośność. Z resztą na Windowsie/Linuksie Twój program też nie zajmuje się obsługą karty graficznej czy pakietami TCP/IP. Od tego jest system operacyjny i sterowniki. Kompilator interesuje przede wszystkim rdzeń mikrokontrolera, lista rozkazów (np. Thumb2) czy dostępność FPU. Do tego opcje linkera, jak adresacja RAM/Flash oraz dołączanie bibliotek. Nie ma znaczenia SysTick czy RTC. Możesz zerknąć na pliki Makefile dla różnych ARMowych projektów i sprawdzić jakie parametry są przekazywane do kompilatora.

W AVR GCC jest taka funkcja: FILE* fdevopen(int(*)(char, FILE *)put, int(*)(FILE *)get); Służy ona do definiowania strumienia we/wy. Podajesz jej jako parametry wskaźniki na funkcje do wysyłania i odbierania znaku. Parametry mogą być NULami. I jest taki myk, że pierwsze wywołanie fdevopen(), które ma parametr put niebędący NULem, powoduje zdefiniowanie strumienia stdout (standardowego wyjścia). Jeśli więc masz funkcję uart_putc(), czyli dla pojedynczego znaku, to możesz napisać: int put(char c, FILE * file) { uart_putc(c); return 0; } I wywołać fdevopen(): fdevopen(&put, NULL); Od tego momentu możesz używać printf() a wyniki jego działania będą szły na port szeregowy. Albo np. na LCD jeśli zamiast uart_putc() użyjesz funkcji do wyświetlania literki na wyświetlaczu. Możesz też zdefiniować drugi strumień i odwoływać się do niego za pomocą fprintf() jeśli chcesz wysyłać teksty metodą printfową zarówno na LCD jak i UART. To wszystko jest opisane w dokumentacji AVR GCC, a dokładniej AVR libc

formatting link
Co do czasu, to AVR libc ma wewnętrzny licznik, który trzeba inkrementować wywołując co sekundę funkcję system_tick(). A najpierw oczywiście zainicjalizować wołając set_system_time(). Wtedy funkcja time() będzie mogła zwrócić właściwy czas. Opis jest na stronie
formatting link
Jak widać obsługa czasu jest okrojona. Nie można np. ustawić strefy czasowej oraz czasu letniego za pomocą zmiennej środowiskowej. Zamiast tego ustawia się strefę funkcją set_zone() a czas letni jest załatwiany poprzez wskazanie funkcją set_dst() funkcji, która obliczy przesunięcie czasu letniego na podstawie podanego timestampa (przykładowo dzisiaj zwróci 3600 a w grudniu

0). Czyli jeśli wywołamy funkcję localtime(), to zostanie wzięty czas z funkcji time(), dodane przesunięcie wynikające ze strefy czasowej (podane jako parametr set_zone()) oraz dodane przesunięcie z funkcji wskazanej przez set_dst(). A skąd wziąć funkcję do czasu letniego? W bibliotece util/eu_dst.h jest taka funkcja dla Unii Europejskiej. Ale nie działa poprawnie :) Bug+fix:
formatting link
Czyli podsumowując, w AVR GCC zamiast syscalli wymyślili takie obejścia.
Reply to
Grzegorz Niemirowski

Hmm... Wygląda na to, że problem leży głębiej i dotyczy raczej RTC (ewentualnie funkcji bibliotecznych odpowiedzialnych za odczytywanie czasy), niż biblioteki standardowej time.h.

Zgodnie z sugestiami, które tu padły, zastąpiłem usunąłem swoją własną wersje funkcji time() i napisałem własną wersję _gettimeofday.

int _gettimeofday (struct timeval* tp, struct timezone* tzp) { RTC_TimeTypeDef timeStruct; RTC_DateTypeDef dateStruct; struct tm dstTime;

HAL_RTC_GetTime(&hrtc, &timeStruct, RTC_FORMAT_BIN); HAL_RTC_GetDate(&hrtc, &dateStruct, RTC_FORMAT_BIN); dstTime.tm_hour = timeStruct.Hours; dstTime.tm_min = timeStruct.Minutes; dstTime.tm_sec = timeStruct.Seconds; dstTime.tm_year = dateStruct.Year + 100; dstTime.tm_mon = dateStruct.Month - 1; dstTime.tm_mday = dateStruct.Date;

if (tp) { tp->tv_sec = mktime(&dsttime); tp->tv_usec = 0; }

if (tzp) { tzp->tz_minuteswest = 0; tzp->tz_dsttime = 0; }

return 0; }

Następnie w pętli głównej usunąłem gmtime(), zamiast tego wyświetlając na LCD aktualna wartość zwracaną przez time(). Efekt był dość... Dziwny... Mianowicie liczba złożona z dwóch ostatnich cyfr faktycznie zwiększała swoją wartość o jeden co sekundę. Natomiast trzecia, czwarta i piata cyfra od końca co chwilę zmieniała swoją wartść "tam i z powrotem" - raz było 500 z czymś, potem ponad 600, potem znów 500 z czymś i tak dalej...

Postanowiłem więc zrobić eksperyment i stworzyłem zmienną uint32_t _rtc, która była zwiększana o 1 w przerwaniu alarmu RTC. Podpiąłem ją do funkcji _gettimeofday i problem zniknął.

Ktoś wie gdzie może leżeć przyczyna takiego zachowania? Co robię nie tak czytając RTC? Przykład u góry.

Reply to
Atlantis

On 09/13/18 01:46, Atlantis wrote: [...]

To mocno śmierdzi pisaniem po stosie, i problem wcale nie musi być w twoim kodzie. Uprość maksymalnie program testowy, nie używaj LCD ani innych wodotrysków, tylko pisz na konsolę szeregową. Jeśli w dalszym ciągu będą problemy, to błąd najprawdopodobniej siedzi gdzieś w obsłudze RTC. Jeśli nie, to dodawaj po kolejne elementy i patrz kiedy zacznie wariować. Wtedy możesz zacząć szukać w którym komponencie jest problem.

Nie bez znaczenia jest też wersja kompilatora. Dwa razy się mocno przejechałem na arm-gcc: Po raz pierwszy, kiedy próbowałem skompilować bibliotekę do obsługi launchpadowego LCD od TI. Darmowy kompilator wszystko pięknie kompilował, przykłady się linkowały i uruchamiały, nie działał tylko ekran dotykowy. Udało mi się zawęzić region gdzie powstaje błąd do kilku linijek kodu, i tam utknąłem. Nie było tam żadnych sztuczek zależnych od wersji kompilatora, nic do czego można by się przyczepić. Po skompilowaniu gcc od TI wszystko zadziałało poprawnie. Drugi raz wyłożyłem się na klasach z metodami wirtualnymi. Wystarczyło dodać słówko "virtual" do deklaracji metody i program szedł w maliny. Tym razem śledziłem wykonanie instrukcja po instrukcji, i znalazłem że momencie powrotu z metody wskaźnik stosu wskazywał w jakieś losowe miejsce w pamięci. I znów, z gcc od TI i ich własnym kompilatorem wszystko działało poprawnie. Ale wystarczyła sama zmiana wywoływanego kompilatora w projekcie wygenerowanym przez CCS żeby problem powrócił. W tym momencie się poddałem, a projekt czeka aż będę miał czas i wenę żeby znów się nim zająć.

Jacek.

Reply to
Jacek Radzikowski

Płytka prototypowa na której działa ten przykład jest dość skromna - to fakt. Flash jest w chwili obecnej prawie całkowicie zapchany, jednak pamięci RAM pozostało jeszcze całkiem sporo. Wątpię, żeby mogło dojść do napisania stosu. Biblioteka LCD działała prawidłowo na AVR, a po przeportowaniu na STM32 program z nią również działa poprawnie, pod warunkiem zastąpienia wbudowanego RTC osobną zmienną przechowującą timestampa.

Jedyne co mi jeszcze przychodzi do głowy, to próba odczytywania RTC z parametrem RTC_FORMAT_BCD, a następnie konwertowania do postaci binarnej za pomocą zestawu własnych funkcji.

W każdym razie sposób w jaki pobieram dane z RTC i wypełniam nimi strukturę struct tm wygląda w porzadku?

Reply to
Atlantis

Z ciekawości zobaczę też chyba jak ten projekt zachowa się po przeniesieniu na jakąś "większą" płytkę Nucleo albo Discovery. Tylko muszę znaleźć na to chwilę czasu. ;)

Reply to
Atlantis

Ilość dostępnej pamięci nie ma żadnego znaczenia. Wystarczy zaalokować o

1 bajt za mało na dane i struktury w pamięci zaczynają na siebie nachodzić. Nawet jeśli ponad nimi będzie jeszcze kilka MB nieużywanej pamięci. To że biblioteka działa na jednej platformie że oznacza że będzie działać na innej. Dlatego do debugowania zawsze powinno się używać jak najprostszego kodu testowego, z minimalną ilością zależności od dodatkowych komponentów.

Poza tym że nie pochwaliłeś się jak inicjalizujesz hrtc, to na pierwszy rzut oka nie ma do czego się przyczepić.

Jacek.

Reply to
Jacek Radzikowski

Atlantis snipped-for-privacy@wp.pl napisał(a):

Nie inicjalizujesz wszystkich pól tej struktury, np. tm_isdst. Wyzeruj ją: przy deklaracji struct tm dstTime = {0};

Przy okazji: zawsze używaj time_t bo nie masz gwarancji, że timestamp będzie

32-bitowy. To się może zmieniać w zależności od wersji kompilatora.

Moim zdaniem czytasz dobrze. Czemu nie wyświetlisz sobie poszczególnych pól na LCD albo serialu? Im więcej danych diagnostycznych tym lepiej :)

Reply to
Grzegorz Niemirowski

Spróbowałem nawet zerowania struktury za pomocą funkcji memset, ale to chyba nie to. Mam jeszcze jedną hipotezę - zauważyłem, że podczas ustawiania zegara na początku pracy programu (kod wygenerowany przez STM32CubeMX) podawane są również dodatkowe opcje (np. coś związanego ze zmianą czasu) a także dzień tygodnia. W swojej funkcji synchronizującej czas pominąłem te linijki. Po powrocie do domu zobaczę, jak będzie się zachowywał uzupełniony kod.

Tak BTW przyszedł mi do głowy jeszcze jeden pomysł - z tego co pamiętam w niektórych modelach PIC32 przed zmianą ustawień zegara konieczne było odblokowanie tej możliwości poprze wpisanie odpowiedniej wartości do jednego z rejestrów. Może coś takiego ma też miejsce przynajmniej w niektórych STM32? W takiej sytuacji oczekiwałbym jednak, że autorzy HAL wzięli to pod uwagę. Może jednak trzeba to zrobić osobno?

Hmm... Przecież chyba właśnie na tym polega sens stosowania typów zmiennych w formacie *int*_t? Rozumiem, gdybym użył typu unsigned long, jednak uint32_t 32-bitową zmienną bez znaku? Czyżbym nie miał racji?

Reply to
Atlantis

Atlantis snipped-for-privacy@wp.pl napisał(a):

W takim razie nie wiem. Niemniej ciągle się kłania monitorowanie wartości RTC. To też jest odpowiedź na poniższe dwa Twoje akapity. Bez sprawdzenia poprawności działania RTC nie ma co się w ogóle zajmować time.h.

Zainicjuj zgodnie z samplami. sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE; sTime.StoreOperation = RTC_STOREOPERATION_RESET; Nie zostawiaj niezainicjowanych pól w strukturach.

W STM32 też tak jest i to chyba we wszystkich. Autorzy HAL jak najbardziej o to zadbali. Obejrzyj sobie kod funkcji ustawiających datę i czas. Jest tam wykonywane odblokowywanie rejestrów.

Oczywiście jak najbardziej masz rację, że uint32_t to typ 32-bitowy bez znaku i masz gwarancję, że zawsze tak będzie. Natomiast mnie chodziło o typ time_t. Napisałem kiedyś takie coś: struct tm * t = localtime((time_t *)&seconds); a seconds było zadeklarowane jako uint32_t I to działało poprawnie w GCC 5.4. Natomiast w GCC 7.2 przestało, localtime() zaczęło zwracać bzdury. Dlaczego? Bo time_t zmieniono na

64-bitowy i localtime() pobierało za pomocą wskaźnika nie tylko zmienną seconds ale także 4 bajty leżące obok w pamięci. Gdybym od razu zadeklarował seconds jako time_t to nie byłoby problemu przy zmianie wersji GCC. uint32_t to był nadal uint32_t, ale time_t zmieniono z uint32_t na uint64_t. Pewnie w Twoim kodzie nie ma takiego problemu, ale pomyślałem, że warto wspomnieć.
Reply to
Grzegorz Niemirowski

Z tego co kojarzę time_t w (g)libc nigdy nie gwarantował typu 32uint i było to "platform dependent".

Reply to
Marek

ElectronDepot website is not affiliated with any of the manufacturers or service providers discussed here. All logos and trade names are the property of their respective owners.