niedziela, 13 lutego 2022

STM32G0 - Bare Metal - MyClock - FreeRTOS

Moje podekscytowanie sięga zenitu mając na uwadze, że zbudowałem własnymi rękami zegarek o docelowo ponad przeciętnych możliwościach oraz to, że wykorzystam w nim RTOS-a i użyję do tego profesjonalnych narzędzi . Czuję taką moc jak młody Jedi :). Czas zacząć mój pierwszy praktyczny projekt pod kontrolą systemu czasu "rzeczywistego" FreeRTOS. Będę popełniał błędy, będę analizował i dociekał, będę się uczył na żywym organizmie .Nie wiem czy docelowo uda mi się całe przedsięwzięcie bo moja wiedza jest tylko hobbystyczna. Ale kto powiedział, że droga na szczyty jest łatwa. Czas wyjść z piaskownicy na szerokie wody. Moja przygoda z RTOS właśnie się zaczyna :). Projekt stawiam od zera w stylu Bare Metal , bez HAL, bez glutenu i bez polepszaczy od STM-a :). Artykuł będzie formą zapisków z uruchomienia projektu zegarka ,ale w trakcie pisania zobaczymy jak to wyjdzie docelowo.

Przed włączeniem RTOS-a do projektu skupiłem się na uruchamianiu poszczególnych komponentów/modułów znajdujących się na pokładzie mojego zegarka tj :

  • moduł sterowania wyświetlaczem 7-seg. LED oparty o MAX7219,
  • moduł zegarka oparty o MCP79410,
  • moduł kontrolera dotyku oparty o  CAP1293,
  • moduł czujnika temperatury oparty o DS18B20,
  • moduł UART dla komunikacji z płytką rozszerzeniem,
  • moduł ADC dla czujnika jasności.

Każdy z wymienionych modułów funkcjonalnych będzie oddzielnym bytem w aplikacji RTOS. Do modułów MCP79410 i CAP1293 mam stworzone interfejsy oparte o strukturę callbacków, których kolejne  zalety już dostrzegam przy tworzeniu projektu zegarka. Interfejsy umożliwiają w prosty sposób ogarnięcie wielu funkcjonalności modułu za pomocą odwołania do jednego obiektu , ogromnie to ułatwia poruszanie się po wielu modułach w obrębie projektu. Moja baza hardware/software  :

  • płytka z modułem zegara opartym o STM32G071 
 

  • programator J-LINK EDU Mini firmy Segger
 
 

  • środowisko SEGGER IDE
 
 

  • Ozone firmy Segger
 
 
 
  • System View firmy Segger
 

 

Pierwszą czynnością po utworzeniu nowego projektu w IDE Segger jest dołączenie biblioteki FreeRTOS. Jak to zrobić pisałem w jednym z poprzednich artykułów. Ponieważ kluczowym zagadnieniem w RTOS są priorytety, dlatego kilka słów na ten temat się należy. W rozważaniach na temat RTOS występują dwa rodzaje priorytetów . Priorytety wynikające z obsługi przerwań sprzętowych a zawarte w module NVIC oraz priorytety zadań w RTOS. Są to dwa różne systemy priorytetów, które często są mylone i wrzucane do wspólnego worka. W NVIC najwyższy priorytet ma cyferka 0, w RTOS cyferka 0 to najniższy priorytet. W pliku konfiguracyjnym FreeRTOS określamy maksymalną liczbę priorytetów np :

#define configMAX_PRIORITIES   5

Oznacza to, że dostępna pula priorytetów dla zadań RTOS jest w zakresie 0....4, gdzie cyfra 4 oznacza najwyższy dostępny priorytet. Ponieważ RTOS korzysta również z przerwań NVIC-a dlatego w konfiguracji RTOS musimy uwzględnić pewne aspekty z tym związane. Jednym z takich aspektów jest np. określenie z jakiej puli priorytetów NVIC, będzie korzystał RTOS lub inaczej jaka pula priorytetów NVIC dla RTOS jest dozwolona. Gdyby RTOS miał we władaniu całą pulę priorytetów dla przerwań sprzętowych dla siebie, to by zdominował sprzęt w naszym MCU a to jest sytuacja niepożądana. Aby przydzielić pulę priorytetów NVIC dla RTOS, musimy wiedzieć jaką pulą dysponujemy w naszym konkretnym MCU. W rdzeniach opartych o Cortex M0+ mamy dosyć ubogą pulę priorytetów i co ciekawe nie mamy dostępnych sub-priorytetów, trochę lipa. Nie miałem pojęcia, że to tak słabo wygląda w tym rdzeniu. 

Zgodnie z dokumentacją w STM32G071 mamy do dyspozycji 4 poziomy priorytetów NVIC dla przerwań,  zakres 0....3. I z tej mini-puli musimy oddać do dyspozycji dla RTOS jakąś część. Ja oddam 50% czyli priorytety 2 i 3 dla RTOS. Priorytet 0 i 1 zostawiam we władaniu sprzętu. W takim układzie przerwania NVIC z priorytetem 0 lub 1 mają moc wywłaszczenia działań RTOS

Idąc dalej tropem braku dominacji RTOS nad sprzętem, musimy mieć pewność, że przerwania z ktorych core RTOS korzysta, takich jak SysTick, PendSV czy SVCall ,nie są przypadkiem na najwyższych priorytetach NVIC . Co do zasady w przypadku użycia RTOS, musimy je przesunąć do najniższego dostępnego priorytetu NVIC. W moim przypadku wszystkie wymienione wyżej przerwania używane przez core RTOS będą skonfigurowane z jednym priorytetem NVIC nr 3. Całość konfiguracji dokonujemy w pliku konfiguracyjnym RTOS i użyjemy do tego poniższych dyrektyw  :


configKERNEL_INTERRUPT_PRIORITY przypiszemy wartość 3  czyli najniższy możliwy priorytet NVIC jaki oddajemy do dyspozycji RTOS. Ta deklaracja jest używana do przypisania priorytetów przerwaniom używanym przez RTOS i na tym priorytecie NVIC lokalizowane są przerwania SysTic, PendSV, SVCall.

configMAX_SYSCALL_INTERRUPT_PRIORITY przypiszemy wartość 2 . Czyli najwyższy priorytet NVIC w dyspozycji RTOS. Wszystkie przerwania z priorytetami 0 lub 1 nie mogą być użyte w API systemowym RTOS (w moim przypadku). Przerwania z pozostałymi priorytetami mogą używać API systemowych oznaczonych jako FromISR i mogą być zagnieżdżane. Nawet sekcje krytyczne RTOS mogą być przerwane przez przerwania z priorytetami od 0-1.

 STM32G071 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* Cortex-M specific definitions. */

#define configPRIO_BITS         __NVIC_PRIO_BITS // priority levels in CMISIS, STM32G0 value 2

 
/* The lowest interrupt priority that can be used in a call to a "set priority"
   function. */
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY   0x03
 
/* The highest interrupt priority that can be used by any interrupt service
   routine that makes calls to interrupt safe FreeRTOS API functions.  DO NOT CALL
   INTERRUPT SAFE FREERTOS API FUNCTIONS FROM ANY INTERRUPT THAT HAS A HIGHER
   PRIORITY THAN THIS! (higher priorities are lower numeric values. */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 2
 
/* Interrupt priorities used by the kernel port layer itself.  These are generic
   to all Cortex-M ports, and do not rely on any particular library functions. */
#define configKERNEL_INTERRUPT_PRIORITY   ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
/* !!!! configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to zero !!!!
 #define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

Powyżej wycinek pliku konfiguracyjnego RTOS z moimi ustawieniami. Widzimy tam jakieś dziwne przesunięcia. Wynikają one z faktu, że każdy kanał przerwania NVIC ma do dyspozycji rejestr 8-bitowy opisujący priorytet przerwania. W naszym przypadku priorytet wykorzystuje dwa bity (bo maksymalna liczba priorytetów 0...3) i są to dwa najstarsze bity .Warto w tym momencie doczytać sobie jak NVIC od strony rejestrów wygląda.

Na początku dodam do aplikacji RTOS obsługę modułu zegarka MCP79410.  Tworzę zatem pierwszy Task RTOS o nazwie vClockTask .W Tasku umieszczam na początku inicjalizację MCP79410 a wniej m.in generowanie fali 1 Hz na pinie MFP, zasilanie bateryjne i czas startowy. Interesują mnie sekundy, minuty i godziny. Dzień i rok jest mi na razie zbędny. Co 1 Hz będzie generowane przerwanie EXTI z pinu MCU PB5 . Przerwanie to służyć będzie m.in do synchronizacji wyświetlania czasu w module wyświetlacza LED. Task vClockTask będzie zatem przez 1 sekundę "zamrożony" i będzie w stanie Blocked. Poniżej możliwe stany Tasku w FreeRTOS :


Wybudzenie Tasku vClockTask nastąpi po 1 s , kiedy wywołane zostanie przerwanie EXTI od pinu nr PB5 na którym jest MFP od strony zegarka MCP79410.

Dobrą praktyką przy tworzeniu tasków RTOS jest sprawdzenie czy task poprawnie się utworzył i czy przypadkiem nie zabrakło na jego utworzenie pamięci. W tym celu możemy użyć bibliotecznej funkcji assert. Wystarczy tylko dołączyć  #include <assert.h>. We FreeRTOS jest również assert zaimplementowany ale korzystam w projekcie z bibliotecznej funkcji. Zerknijmy zatem jak takiego sprawdzenie w kodzie dokonać :


Jeśli zadanie nie zostanie utworzone czyli nie otrzymamy po utworzeniu Taska zwrotnie wartości pdPASS to w oknie Terminal Debugger w trybie debugowania programu, otrzymamy poniższą informację (patrz okno na dole):


Informacja wskazuje w jakiej linii w jakim pliku i jaki Task nam nie spełnił zależności. Celem symulacji podstawiłem cyfrę 4 do sprawdzanego warunku . W docelowym i sprawdzonym programie funkcję assert wyłączamy przez aktywację zapisu :

 #define NDEBUG // activate if not use Assert control, before #include <assert>

Bardzo fajna jest funkcjonalność assert i warto posługiwać się tym dobrodziejstwem. No dobrze mam utworzony Task vClockTask , na razie pusty z usypianiem Task-u na 500 ms :


Muszę sprawdzić czy zegarek MCP79410 prawidłowo się inicjalizuje i czy  przerwanie EXTI jest generowane co 1 s . W tym celu wykorzystuję diodę LED jaką mam na pokładzie zegarka i "togluję" ją w przerwaniu :


Poniżej obrazek z definicjami jakich używam do obsługi LED :


Jedna ważna rzecz . Ponieważ przerwanie EXTI będzie wykorzystywane przez RTOS ,więc priorytet  tego przerwania musi być w puli 2...3 (bo taką pulę wcześniej przydzieliłem dla RTOS) . Wybieram priorytet 2 dla tego przerwania i taki ustawiam w NVIC.


Odpalam program sprawdzam. Dioda LED miga co 1 s co oznacza, że zegarek MCP79410 żyje i generuje mi przebieg prostokątny 1 Hz na pinie MFP (PB5 od strony MCU) . Brawo, jeden kamień milowy mam z głowy. 


Teraz w utworzonym vClockTask użyję semafora, który będzie zwalniany co 1 s w przerwaniu EXTI i co 1 s zadanie będzie odblokowane na czas wykonania  swojej funkcjonalności, której nie mam jeszcze na tym etapie zaimplementowanej. W zadaniu tym docelowo będę odczytywał czas z rejestrów MCP79410. Semafor synchronizuje nam wykonanie zadania w Task-u z przerwaniem EXTI. Czas na doczytanie o semaforach w RTOS. Najszybciej "zakumamy bazę" jak sobie obejrzymy wykres z animacją pod tym linkiem : Link

Chcąc korzystać z semaforów konieczne jest dołączenie plików nagłówkowych queue.h oraz semphr.h.  Tworzymy uchwyt do semafora, robimy to jak poniżej :


Na zadeklarowanym uchwycie semafora o nazwie xSemaphoreClockTask tworzymy semafor binarny w zadaniu vClockTask, wiersz poniżej sprawdzamy za pomocą assert czy semafor został z powodzeniem utworzony



Zwracam szczególną uwagę aby tam gdzie tylko to możliwe przy powoływaniu nowych obiektów RTOS (tasków, semaforów etc), sprawdzać czy obiekty się fizycznie utworzyły jeśli nie, to znaczy , że mamy problemy z zasobami pamięci dla obiektu a to są problemy na których niepotrzebnie będziemy wytapiać czas aby dojść co jest nie tak. 

Poniżej obrazek z użyciem funkcji xSemaphoreTake() , która zabiera nam token. Pobraliśmy token semafora i zablokowaliśmy zadanie vClockTask na nieskończony czas (portMAX_DELAY , w pliku konfiguracyjnym RTOS musi być #define INCLUDE_vTaskSuspend 1), dopóki nie zostanie zwrócony token za pomocą wywołania funkcji xSemaphoreGive() a w przypadku przerwań xSemaphoreGiveFromISR().



Odblokowanie semafora następuje co 1 s w przerwaniu EXTI4_15_IRQHandler



Oddanie tokena semafora powoduje, że zadanie, które na niego czekało przechodzi w stan gotowości i jeżeli ma wyższy priorytet od aktualnie wykonywanego to następuje przełączenie kontekstu. Wewnątrz przerwania nie jest to jednak realizowane automatycznie, dlatego funkcja xSemaphoreGiveFromISR() w odróżnieniu od xSemaphoreGive() przyjmuje jeden dodatkowy argument *pxHigherPriorityTaskWoken. Na jego podstawie możliwe jest określenie czy zadanie, które zostało odblokowane wskutek wywołania xSemaphoreGiveFromISR() ma wyższy priorytet od zadania, które zostało przerwane. Jeżeli tak to *pxHigherPriorityTaskWoken przyjmie wartość pdTRUE i na tej podstawie można wyzwolić przełączenie kontekstu. Służy do tego funkcja portYIELD_FROM_ISR() przyjmująca jeden argument. Jeżeli przekazany do niej parametr ma wartość różną od pdFALSE to zostanie wyzwolony proces przełączania kontekstu. W przeciwnym wypadku nic się nie dzieje i po zakończeniu procedury obsługi przerwania MCU wraca do wykonywania przerwanego zadania.

W zadaniu vClockTask dałem "toglowanie" diody LED i po tym poznam czy pobieranie tokena semafora i zwracanie go w przerwaniu, działa prawidłowo. Na razie wszystko działa zgodnie z przewidywaniem. No dobrze ale jedną diodą LED nie możemy w nieskończoność debugować aplikacji RTOS. Poza tym nie widzimy tego co się dzieje w aplikacji, no to jak mamy nad tym zapanować. Tutaj z pomocą przychodzą nam profesjonalne narzędzia firmy Segger takie jak Ozone i System View. Pisanie aplikacji RTOS bez stosownych narzędzi do debugowania prędzej czy później skończy się porażką i szybko się zniechęcimy do RTOS-a, dlatego też RTOS nie jest zbyt popularny wśród hobbystów moim zdaniem. Poza tym użycie RTOS w projekcie całkowicie zmienia sposób podejścia do projektowania aplikacji  i nie każdy może się w tym odnaleźć.

Zatem zerknijmy na pierwsze narzędzie firmy Segger czyli Ozone. W skrócie jest to taki zewnętrzny debugger, niezależny od IDE z rozszerzeniem dla RTOS. Potrzebujemy oczywiście programatora J-LINK do tego aby działać na narzędziach Seggera.

Ściągamy program Ozone - The J-Link Debugger , znajdziemy go pod tym linkiem : Link
Instalujemy w systemie i uruchamiamy :


Wybieramy opcję Create New Project. W okienku , które się pojawi musimy podać model naszego MCU , typ rdzenia i ściężkę do pliku *.svd, adekwatnego dla naszego MCU. Dla mojego STM32G071 wygląda to jak poniżej : 



Podpinamy J-Linka i wybieramy Next , patrzymy czy programator zostanie wykryty. W moim przypadku wygląda to jak poniżej :


Ponownie wybieramy Next i przechodzimy do okienka w którym musimy podać lokalizację pliku *elf dla naszego projektu. Plik ten jest generowany po każdej kompilacji projektu. W moim przypadku ścieżka do pliku wygląda jak poniżej :


W kolejnym okienku nic nie podajemy :



Uruchomi się program z komunikatem błędu.  Wybieramy Continue i przechodzimy do Save Project As. Wpisujemy nazwę dla pliku startującego program Ozone dla naszego projektu np. OzoneStart. Plik zapisujemy w głównym katalogu projektu, który chcemy debugować za pomocą Ozone :


Zamykamy program Ozone i odszukujemy utworzony plik OzoneStart.jdebug. Plik ten otwieramy w edytorze tekstu i dodajemy do niego zaznaczony poniżej wiersz i zapisujemy zmiany:


Od tego momentu Ozone jest gotowy do debugowania naszego projektu i uaktywniony jest w nim plugin dla FreeRTOS-a. Teraz sprawdzimy jak Ozone wygląda po uruchomieniu. W tym celu podłączamy J-linka do urządzenia , w moim przypadku do mojego Zegara. Jeszcze tu nadmienię , że Ozone można odpalić z poziomu IDE Seggera. Na razie jednak odpalam go jako oddzielną aplikację poza IDE. Uruchamiamy Ozone i wybieramy opcję Open Existing Project, wskazujemy na plik OzoneStart , który wcześniej utworzyliśmy :


Po chwili zobaczymy okno startowe Ozone :


W tym momencie debuger jest jeszcze nie aktywny. Aby wejść w tryb Debugowania klikamy zieloną ikonkę lewy górny róg lub klawisz F5. Pojawi się okienko z informacją o licencji wybieramy "Yes".  Przed kliknięciem w ikonkę uaktywniam widok dla FreeRTOS :


Odpalenie debugowania :



Z Ozone możemy wyciągnąć szereg informacji przydatnych do analizy działania aplikacji FreeRTOS takich jak na przykład zajętość stosu przez taska i wszystkich lokalnych zmiennych taska oraz ich wartości. Ale i wiele innych informacji . Możemy pauzować w dowolnej chwili lub w wyznaczonych miejscach i sprawdzać stany poszczególnych Tasków  etc.  Trzeba to narzędzie poznać. 

Drugim narzędziem przydatnym do pracy z aplikacją FreeRTOS jest System View firmy Segger. To aplikacja w której mamy graficzną reprezentacje działania programu pod kontrolą FreeRTOS .Nie jest to strikte debuger ale bardziej przeglądarka w której śledzimy zachowanie się programu i analizujemy te zachowania. Proces działania programu możemy zapisywać i analizować potem w dowolnym momencie. Problemy mogą być z dołączeniem tego narzędzia do projektu a w szczególności integracja z patchem FreeRTOS. Zostawiam to narzędzie na inny czas, jak bardziej zaprzyjaźnię się z FreeRTOS, na razie wystarczy mi Ozone.

W zadaniu vClockTask powołują strukturę MCP79410_Time_Clock_Task , do której będę pobierał czas odczytany z zegarka. Strukturę tę będę chciał wysłać do zadania (jeszcze na tym etapie nie stworzone) odpowiedzialnego za wyświetlanie czasu na wyświetlaczach LED



Wypełniam strukturę danymi pobranymi z zegarka MCP79410 , korzystam tutaj z mojego interfejsu stworzonego dla wygodnej rozmowy z zegarkiem :



Już na tym etapie zabawy z FreeRTOS bardzo mi się podoba , że każdy kolejny krok w budowie aplikacji należy dokładnie przemyśleć i zaplanować. Od tego zależy w dużej mierze powodzenie działania aplikacji. Na razie czuję dobrą chemię w stosunku do RTOS. Celem jest działająca aplikacja, ale to nie cel sprawia najwięcej satysfakcji w tej zabawie, ale droga do osiągnięcia  tego celu.

Mam task w którym odczytuję czas z zegarka MCP79410. Teraz czas na stworzenie taska , którego zadaniem będzie wyświetlanie tego czasu . Muszę zatem dodać do projektu moduł z MAX7219 jako oddzielne zadanie RTOS. Moduł mam osobno przetestowany i obudowany interfejsem. Powtórzę się , odkąd poznałem interfejsy to garściami korzystam z tego dobrodziejstwa. Widzę ogromne zalety interfejsów , które znacząco ułatwiają budowaniu projektu.

W czasie pisania artykułu, projekt mój się rozwija i dokonuje w nim zmian na bieżąco. Nabywam doświadczenia w debugowaniu, wiem już w jakich sytuacjach mogą wystąpić Hard Faulty

Tworzę task vDisplayTask do wyświetlania czasu na LED


Utworzony task będzie miał priorytet niższy o jedno oczko niż vClockTask. W zadaniu vDisplayTask będę pobierał dane o czasie z zadania vClockTask i wyświetlał te dane na wyświetlaczu LED. Do przesyłania danych wykorzystam FreeRTOS-ową kolejkę Queue. Deklaruję zatem taką kolejkę :


Definicję kolejki wykonuję w zadaniu vClockTask bo to zadanie będzie do kolejki wrzucać dane  i wysyłać je do vDisplayTask :


W sumie definicja kolejki w zadaniu vClockTask nie jest do końca dobrym pomysłem bo w przypadku zrównania priorytetów zadań vClockTaskvDisplayTask otrzymamy ładny Hard Fault., którego przyczyną będzie brak definicji kolejki przed wywołaniem zadania vDisplayTask .

Wrzucamy dane do kolejki :


Dane to czas w podziale na SEC, MIN, HOUR. W zasadzie do szczęścia potrzebuję tylko MIN HOUR . Dane odczytuję co 1 sekundę i zapisuję w strukturze MCP79410_Time_ClockTask. Po czym cała struktura pakowana zostaje do kolejki xQueueClockTask .Teraz przyjrzyjmy się jak wygląda zadanie vDisplayTask w którym wyświetlamy dane odebrane z zadania vClockTask


Na początku zadania powołuje strukturę MCP79410_Time_DisplayTask do której będą zapisane dane odebrane z kolejki xQueueClockTask. Potem sprawdzam czy są jakieś dane w kolejce przesłane przez zadanie vClockTask ,jeśli tak to przechodzę do wyświetlania danych na wyświetlaczu, czyli wyświetlam minuty i godzinę. Tu warto nadmienić, że parametr portMAX_DELAY powoduje, że zadanie vDisplayTask zostanie zablokowane na nieskończony czas, dopóki nie przyjdzie dana z kolejki do odczytu. W ten sposób dokonuję za pomocą kolejki synchronizacji zadań. Czyli vDisplayTask  wyświetli dane co 1 sekundę, bo z takim czasem odblokuje nam się zadanie vClockTask. Toglowanie wyświetlaniem kropką pomiędzy minutami a godziną stanowi heart beat zegarka. Odebrane dane z MCP79410 są w formacie BCD i w takim formacie też są wysyłane do MAX7219. Musimy wyłuskać część dziesiętną i jedności oraz ustawić to na poszczególnych polach wyświetlacza LED.

Duża zaletą projektowania aplikacji pod FreeRTOS jest modularność , nie muszę ogarniać wszystkich rzeczy naraz, tylko skupiam się na poszczególnych zadaniach i funkcjonalnościach w tych zadaniach.

Do debugowania używam na razie najprostszych metod takich jak Semihosting zaimplementowany w IDE SEGGER-a. Widzę w ten sposób czy zadania, żyją , mogę monitorować wartości zmiennych etc. 

Odpalam program i patrzę czy zegarek żyje i wyświetla to co ma wyświetlać czyli czas :). Wszystko działa jak na razie bez zająknięcia. 

Kolejny kamień milowy mam za sobą . Teraz trudniejsze zadanie mam przed sobą, czyli ustawianie czasu za pomocą dotyku. Ponieważ dotyk czyli CAP1293 i zegarek MCP79410 mają wspólny zasób sprzętowy w postaci I2C1 ,dlatego kluczowym zagadnieniem tutaj jest odpowiednie przydzielanie dostępu do tego zasobu. Na razie muszę dodać moduł dotyku do projektu i stworzyć do niego porządny interfejs. Tu na marginesie dodam, że bez mojej płytki developerskiej dla STM32G0 na której "ożywiałem" poszczególne moduły zegarka nie miałbym szans w uruchomieniu zegarka. Drugim kluczowym elementem w powodzeniu całej operacji był i jest analizator stanów logicznych. Oczywiście gdybym korzystał z gotowych bibliotek HAL dla STM32 nie musiałbym używać dodatkowych narzędzi ale i niczego bym się nie nauczył.

Moduł dotyku CAP1293 mam już dołączony do projektu i stworzony do niego wygodny interfejs. Na razie nie tworzę zadania oddzielnego dla tego modułu. Sprawdzam przy użyciu przerwania czy dotyk działa. Tworzę jednocześnie ochronę dostępu dla funkcji I2C1 za pomocą Mutex. Ponieważ mam dwie funkcje I2C1 niskiego poziomu do zapisu i odczytu , dlatego użyję oddzielnych mutexów dla każdej z nich. Na razie jednak zerknijmy jak wykonałem test dotyku w przerwaniu :


Dotyk generuje przerwanie na które reaguję, na razie bez rozróżnienia , które pole dotknąłem. Funkcja przerwania obsługuje jednocześnie zegarek MCP79410 , na razie nie widzę tutaj jakiegoś problemu.
Dotknięcie , któregokolwiek pola uruchamia diodę LED. Długie przytrzymanie pola, dioda LED miga.
Docelowo w przerwaniu wyrzucę kasowanie flagi przerwania w module dotyku, takich wolnych transmisji jak I2C w przerwaniach nie używamy. Na razie wszystko działa ładnie. Pole dotykowe "DOWN" najbardziej odległe od CAP1293 delikatnie mi szwankuje, będę musiał pogrubić ścieżki do tego pola dotyku, ale to nie problem. Teraz zabieram się za Mutex i ochronę funkcji  I2C

Zrobiłem wiosenne porządki w kodzie, wszystkie deklaracje zasobów FreeRTOS takich jak zadania, semafory , mutexy, kolejki wywaliłem z pliku main.c i stworzyłem dla nich oddzielne pliki. Łatwiej będzie mi zapanować nad tym wszystkim w przyszłości jak aplikacja się rozbuduje. 

Deklaracja wskaźników na Mutex :


Definicja Mutex :




Użycie mutexów w ochronie zasobów I2C na przykładzie funkcji zapisującej:


Na końcu funkcji, mutex trzeba oddać, jeśli tego nie zrobimy zablokujemy proces przy ponownym wejściu.

Ważne dwie uwagi . Pierwsza to taka aby mutexy jakimi otoczyliśmy ochronnie funkcje I2C  były stworzone przed inicjalizacją I2C. Inaczej zablokujemy program. U mnie wygląda to tak , że tworzę wszystko co wykorzystuje aplikacja ze strony FreeRTOS-a , odpalając funkcje tworzące przed konfiguracją sprzętu.:


Druga ważna uwaga, która wyszła mi w boju to taka, aby koniecznie dodać opóźnienie po funkcji Segger-a konfigurującej zegary :


Do tej chwili wszystko działa płynnie i bez zająknięcia.  Coraz bardziej podoba mi się tworzenie aplikacji pod RTOS ,ale jednocześnie z tyłu głowy mam , że debugowanie takiej aplikacji może być w niektórych przypadkach bardzo trudne. Szczególnie jak aplikacja rozrośnie się i przybędzie semaforów, mutexów , kolejek. Kiedy natkniemy się na deadlocki, hard faulty , zagłodzenia zadań. Czuję jednocześnie ogromną moc i ogromne możliwości tworzenia aplikacji za pomocą RTOS. Nie straszne mi są z RTOS aplikacje z dużą liczbą współbieżnych zadań.

RTOS jest pięknym narzędziem i podejściem do programowania współbieżnego ale zdecydowanie odradzam, osobom początkującym w programowaniu , branie się za niego, bo to może odbić się na ich zdrowiu psychicznym :). 

Mam już, działający moduł dotyku, czas stworzyć dla niego zadanie. Na razie, w zadaniu będę tylko kasował flagę przerwania od CAP1293 ,tak aby takie kasowanie nie odbywało się w przerwaniu EXTI. Potem zastanowię się jakie funkcjonalności w tym zadaniu będą realizowane przez dotyk.

Tworzę zadanie dla dotyku vTouchTask:



Zadanie dotyku działa . Wszystko jest wykonywane płynnie i bez zająknięcia a szczególnie widać to na funkcji sprzętowej auto-repeat dotyku. Muszę jednak w tym momencie włączyć myślenie RTOS-owe . W zadaniu vClockTask posłużyłem się semaforem do odblokowania zadania , odblokowanie następowało z przerwania. Dopóki token semafora nie został odebrany przez zadanie dopóty zadanie jest zablokowane i nie bierze udziału w bieżącym życiu aplikacji. W zadaniu vTouchTask odezwało się nie RTOS-owe podejście do programowania czyli użyłem zwykłej chamskiej flagi ustawianej w przerwaniu a odpytywanej w zadaniu vTouchTask. Tak nie robimy w aplikacji RTOS !!!! bo to przeczy całej idei współbieżności implementowanej przez RTOS. Dlatego musimy zmienić kod dla zadania vTouchTask. Zmiana będzie polegała na użyciu  Task Notify , czyli kolejnego narzędzia z biblioteki RTOS. Nie będę rozwijał teorii na ten temat bo jest jej dużo w necie ja pokażę jak tego użyć w praktyce w aplikacji. Zatem przy pomocy Task Notify tworzymy mechanizm powiadamiania zadania o wystąpieniu zdarzenia w przerwaniu, związanego z dotykiem. Zwrócę tutaj uwagę , że API CMISIS dla FreeRTOS-a jakim jesteśmy zmuszeni posługiwać się w STM32 CUBE IDE jest mocno wykastrowana w stosunku do natywnego API FreeRTOS, pozbawiona m.in tablicowania dla Task Notify

Najpierw dokonajmy zmian w przerwaniu z którego będziemy wysyłać wiadomość o zdarzeniu . Adresatem tej wiadomości będzie zadanie vTouchTask, jeśli taka wiadomość zostanie odebrana zadanie zostanie obudzone i przejdzie w stan gotowości do wykonania. Idea podejścia w programowaniu z użyciem RTOS jest taka, że zadanie , które oczekuje na coś, czy to na jakąś daną czy na upływ czasu czy na zdarzenie jest zablokowane i nie wykonuje się dopóty nie przyjdą te elementy na które zadanie czeka. Zwalnia się przez to cykle MCU, który może się zajmować innymi zadaniami, które trzeba aktualnie obsłużyć.
 

W xTaskNotifyFromISR() ustawiam bit nr 0 i wysyłam tę informację do zadania vTouchTask . Za pomoca Task Notify możemy przesyłać 32 bitową wartość lub 32 flagi bitowe. Mechanizm Task Notify jest o 40% szybszy niż Semafor i zajmuje mniej miejsca w RAM ,ale przesyłanie informacji następuje tylko do jednego wybranego zadania. Ważny drobiazg musimy mieć na uwadze, mianowicie w Task Notify odwołujemy się do jednego wybranego zadania za pomocą uchwytu (handlera) a nie nazwy zadania. Uchwyt jest wskaźnikiem o typie TaskHandle_t .W moim przypadku uchwyt nosi nazwę  xTouchTaskHandle. Musimy go sobie przed utworzeniem zadania/taska  zadeklarować i podać jako ostatni argument w funkcji tworzącej zadanie xTaskCreate().

Teraz zerknijmy na stronę odbiorczą dla Task Notify z przerwania. Odbierać będzie zadanie vTouchTask :


Po stronie odbiorczej posługujemy się zapisem xTaskNotifyWait() z podanymi argumentami. Interesuje nas w odbiorze bit nr 0 i ten bit będzie na wyjściu kasowany automatycznie. Czyli to jest nasza "freertosowa" flaga z przerwania z automatycznym kasowaniem. Bieżąca wartość Task Notify będzie przechowywana w 32-bitowej zmiennej notificationvalue. Ostatni argument portMAX_DELAY, informuje nasze zadanie, że dopóki nie otrzymamy czegoś z Task Notify to zadanie ma być zablokowane i nie marnować cykli MCU. W zależności co otrzymamy z Task Notify , podejmujemy odpowiednie działania.
 
Teraz chwila na przemyślenie jak w zadaniu dotyku, zrealizować funkcjonalności związane z ustawianiem czasu i obsługą tego procesu. Najpierw stworzę funkcjonalność pola dotykowego "SELECT", pierwszy dotyk tego pola spowoduje mignięcie segmentu wyświetlania minut a drugi mignięcie segmentu z godzinami. Czyli sygnalizacja , która zobrazuje nam , które pole (minuty lub godziny) są wybrane do dokonania na nim zmian. Sygnalizację zrealizowałem za pomocą Task Notify. Po wykryciu dotyku w przerwaniu, zadanie vTouchTask jest odblokowane i sprawdzam w nim jakie pole zostało dotknięte. W przypadku wybrania pola SELECT kod będzie wyglądał następująco :


Najpierw wykrywany jest fakt dotyku (ogólnie), potem jest kasowana flaga dotyku w CAP1293 i musi to nastąpić na początku sekcji nie na końcu. Następnie odpytujemy CAP1293 ,które pole zostało dotknięte. Jeśli dotknięte zostało pole SELECT to wysyłamy o tym informację do zadania vDisplayTask za pomocą funkcji  xTaskNotify() , ustawiony bit nr 0 przy pierwszym dotknięciu a przy drugim bit nr 1 i tak w " koło Macieju ". Zadanie vDisplayTask w zależności od ustawionego bitu w odebranym Task Notify gasi na 1 s pole minut lub godzin. Czyli w ten sposób mamy zaimplementowany mechanizm selektora do ustawiania czasu. Działa to bardzo sprawnie i bez zająknięcia, co mnie cieszy. Kolejny kamień milowy mam z głowy. 

Odnośnie dotyku to mam w zegarku trzy pola dotykowe SELECT, UP, DOWN , warto jednak wspomnieć o fajnej możliwości jaką Microchip  zaimplementował w CAP1293 . Mianowicie multi-dotyk. Czyli za pomocą trzech pól mogę uzyskać dodatkowe funkcje po wybraniu dowolnej kombinacji pól dotykowych.

Selektor wyboru pola minut lub godziny mam zrealizowany . Teraz czas na fizyczną regulację czasu minut i godzin. Obsługę ustawiania czasu realizuję w zadaniu vTouchTask. Zadanie to jest wyzwalane informacją otrzymaną z przerwania a przekazaną przez Task Notify. Jeśli naciśniemy dowolne pole dotykowe to zadanie vTouchTask jest wybudzane. W zadaniu wykrywamy jakie pole zostało dotknięte i odpowiednio reagujemy na to. Poprzednio pisałem obsługę dla pola dotykowego SELECT ,teraz zajmę się kodem dla obsługi pola dotykowego UP czyli zwiększam minuty lub godzinę.


Poniżej ta sama sekcja ale z uproszczeniem kodu i opakowaniu go w wygodny interfejs :


Po wykryciu dotyku pola UP, ustalam za pomocą wartości z touch_SELECT_counter jakie pole zostało wcześniej wybrane za pomocą dotyku SELECT - pole minut czy godzin. Jeśli wybrane zostało pole minut wchodzimy w sekcję kodu dla case selectMinute: a tam pobieramy z MCP79410 aktualną wartość minut. Pobrana wartość minut jest w kodzie BCD dlatego musimy pamiętać o konwersji z BCD na DEC , bo na wartości DEC będziemy operować przy zwiększaniu wartości. Zwiększamy minuty o 1, przesyłamy nową wartość (w BCD) do MCP79410. Potem wyświetlamy nową wartość minut. Wyświetlanie do obsługi ustawiania czasu dałem w zadaniu vTouchTask , ale skoro wydzieliłem do wyświetlania dedykowane zadanie vDisplayTask ,to docelowo tam to wyświetlanie przeniosę. 
 
Na razie kod nie jest produkcyjny ale testowy, skupiam się na uzyskaniu funkcjonalności i testowaniu tych funkcjonalności, potem przyjdzie czas na porządkowanie kodu i optymalizację.

Pole dotyku SELECT i UP pięknie mi chodzą, nie ma żadnych fałszywych dotknięć. Dotknięcia są wykrywane pewnie i stabilnie, żadnych drgań styków bo ich tu nie ma . Odczuwam potęgę interfejsów dotyku Microchipa.  Przytrzymując pole dotyku UP aktywuje się automatycznie sprzętowe auto-repeat i zmiana minut lub godzin zachodzi bardzo płynnie . Czas dla auto-repeat mogę regulować w szerokim zakresie w CAP1293. Życie z dotykiem jest piękne :) koniec z mechanicznymi przyciskami.

W sumie to nawet nie marzyłem jeszcze jakiś czas temu , że za pomocą RTOS-a dojdę tak daleko w projekcie mojego zegarka :). RTOS to była jakaś odległa abstrakcja. Jestem z siebie dumny, no ale kupa roboty/zabawy jeszcze przede mną . Dołączenie modułu odczytu temperatury, modułu ADC do sterowania jasnością wyświetlania a na deser zaprojektowanie płytki z rozszerzeniami dla zegara a tam transciver radiowy, CAN  a może nawet dodatkowy wyświetlacz LCD prezentujący zaprogramowane wydarzenia i wysyłający o tym informację do użytkownika etc.  Kurna ale się wpieprzyłem w robotę :). Z RTOS nie straszne mi jest implementowanie nawet najbardziej niewyobrażalnych pomysłów :).

Ostatnim elementem w obsłudze ustawiania czasu jest oprogramowanie pola dotykowego DOWN. wielkiej filozofii tutaj nie ma :


Pole dotykowe DOWN delikatnie mi szwankuje , potrzebuje więcej czasu na początku do wykrycia dotyku niż pozostałe pola. Być może jakiś problem mam z przelotką na płytce lub ścieżka jest za wąska. Ale to do ogarnięcia temat.

Generalnie kolejny kamień milowy w oprogramowaniu zegarka mam za sobą. Na razie jestem bardzo zadowolony z efektów pracy zegarka. Program pod kontrolą  RTOS działa bardzo stabilnie i bez zacięć.

Czas zająć się czujnikiem temperatury DS18B20. Na pokładzie zegarka mam dwa czujniki . Jeden ma wskazywać temperaturę w pomieszczeniu a drugi na zewnątrz.
Standardowa obsługa DS18B20 wymaga napisania kodu blokującego z dużą ilością delay odmierzanych precyzyjnie w us. Z punktu widzenia FreeRTOS to współpraca z DS18B20 jest porażką i może wykrzaczyć nam aplikację. Rozwiązaniem tego problemu jest symulacja standardu 1-Wire za pomocą UART i wykorzystanie do tego DMA. Można podejść też tak, że do obsługi DS18B20 wykorzystać jakiś mały MCU 8-bitowy i komunikować się np po SPI z MCU "matką". Nie przewidziałem na etapie projektowania płytki , że aplikację będę pisał pod RTOS, dlatego nie przejmowałem się zbytnio tym jakie zagrożenie niesie ze sobą DS18B20. Piny jakie przewidziałem do współpracy z DS18B20 nie można skonfigurować pod UART pomimo, że w STM32G071 mam aż 4 UART. Więc trochę pupa blada. Na razie muszę brnąć w obsługę blokującą i przemyśleć ją to zrobić najmniej boleśnie dla aplikacji a w przyszłości przeprojektować płytkę zegara tak aby pozbyć się blokującej obsługi DS18B20.

Po przedumaniu tematu obsługi DS18B20 , pojawiło się światełko w tunelu na nieblokującą obsługę z wykorzystaniem licznika , przerwania i maszyny stanów.

Zboczę trochę z tematu i pokażę z jakiego rodzaju błędami we FreeRTOS możemy mieć do czynienia.
Stworzyłem sobie nowe zadanie w mojej aplikacji zegara i dodałem je do aplikacji :


Po uruchomieniu aplikacji z nowym zadaniem , aplikacja nie działa. Odpalam debuggera i tam otrzymuję komunikat , że dodane zadanie nie zostało utworzone :



Gdybym nie sprawdzał poprawności utworzenia zadań za pomocą funkcji assert, nie uzyskałbym szybko wiedzy co się nie powiodło i ugrzązłbym w długim szukaniu przyczyny problemu. Wiem , że ostatnie zadanie nie zostało utworzone. W pierwszej kolejności sprawdzam czy nie za mało przydzieliłem pamięci dla zadania ale tu jest OK. Jaki jest stan zajętości pamięci RAM, tu jest OK. Wchodzę zatem w plik konfiguracyjny FreeRTOS i tam szukam możliwej przyczyny.


Głównym podejrzanym jest wielkość sterty przydzielonej dla FreeRTOS. Zmieniam wartość  na większą. Ustawiam 8500 . Aplikacja działa, problem rozwiązany szybko i sprawnie. STM32G071 ma 36 kB RAM-u jest zatem spory luz na szaleństwa.

Wracając do DS18B20 . Celem przyspieszenia prac nad zegarkiem, postanowiłem pójść na skróty i zaaplikowałem na razie blokującą obsługę 1-Wire. Docelowo przeniosę to na UART po przeprojektowaniu płytki . Nie odpuszczę jednak, napisanie maszyny stanów dla DS18B20 , jakieś pierwsze próby takiej maszyny dla 1-Wire mam za sobą i widzę, że ta droga jest do przetarcia. Wymaga to jednak większych zasobów czasu. 
 
Poniżej zaprezentuję jak dodałem do mojej aplikacji RTOS-wej, obsługę czujnika DS18B20. Docelowo w zegarku mam dwa czujniki na oddzielnych torach 1-Wire. Czujniki pracują u mnie w trybie "parasite" czyli z zasilaniem pasożytniczym . Do podłączenia czujnika wystarczą dwa kabelki. Obsługę 1-Wire i DS18B20 napisałem od zera, nie posługując się innymi libsami. Ważna informacja : piny do obsługi 1-Wire w trybie "parasite" nie mogą być skonfigurowane jako "open drain" .

Zacznę od pokazania moich plików do obsługi 1-Wire i DS18B20 :


Do obsługi dwóch czujników (lub więcej) posługuję się strukturą (powołując nowy typ temperatureDevice_t) w której znajdują się informację o porcie i pinie na którym wykonujemy operację oraz pobrane z funkcji GetTemperature(), dane związane z temperaturą takie jak znak, część całkowita i część ułamkowa temperatury. W operacji odczytu temperatury nie posługuję się "floatami" (takie przyzwyczajenie z czasów AVR). Strukturę przekazuję do funkcji 1-Wire i DS18B20 za pomocą wskaźnika. To co warte jest do zakonotowania to sposób w jaki "dobieram" się do parametrów typu port, pin i jak tym posługuję się dalej w funkcjach co zobaczymy w pliku *.c do obsługi DS18B20.


W pliku *c definiuję wstępnie , dwie powołane struktury (dla linii Wire1 i Wire2). Ważne aby wpisać tutaj parametry typu port i pin jakie będą przyporządkowane do obsługi poszczególnych linii 1-Wire. W moim przypadku wykorzystuję dwa piny PA10 i PC6. Do tych struktur funkcja GetTemperature(), będzie pakowała dane o temperaturze.

Poniżej zaznaczone najbardziej elementarne funkcje do obsługi operacji na 1-wire :


W funkcjach tych sprytnie rozwiązałem problem obsługi wielu czujników znajdujących się na różnych liniach 1-wire. Przekazuję w nich wskaźnik na strukturę opisującą parametry linii.

Poniżej kolejne część pliku *c do obsługi DS18B20 :



Na końcu najważniejsza funkcja czyli odczytująca dane o temperaturze z DS18B20 i pakująca je do struktury.


Ponieważ posługujemy się wskaźnikami , więc funkcja nic nie musi zwracać. Filarem języka C są wskaźniki, bez "czucia" ich nasza wiedza jest ułomna. W sumie tak patrząc po sobie to najpierw wiedziałem co to są wskaźniki ale nie widziałem jak je stosować w praktyce i w jakich przypadkach. Dopiero po jakimś czasie po opatrzeniu się na różne kody z zastosowaniem wskaźników coś mi się we łbie otworzyła klapka , że zacząłem "czuć" to. Projekt zegarka pomógł mi uwolnić to "czucie" wskaźników . Stąd prosty wniosek , że aby stymulować nasz mózg musimy robić własne projekty i rozwiązywać realne problemy bo tak najszybciej się czegoś nauczymy.

Teraz najważniejsza część czyli jak dodałem odczyt temperatury do aplikacji RTOS zegarka. W tym celu stworzyłem nowe zadanie w którym będę odczytywał temperaturę z DS18B20:


Ciało zadania do pobierania temperatury na obrazku poniżej :


Tak na marginesie kluczem do panowania nad aplikacją jest rozbicie funkcjonalności na oddzielne pliki , docelowo każde zadanie RTOS wpakuję do oddzielnych plików tak aby uprościć kod w pliku main.c.
Co robi zadanie odczytu temperatury ?. Na wstępie należy wspomnieć, że zadanie jest usypiane na 2.5 s. Po tym czasie najpierw realizowana jest konwersja temperatury a przy kolejnym obudzeniu taska odczytujmy dane z DS18B20. Mechanizm przełączania konwersja/odczyt, realizuje mi flaga - flags . Zwróćmy uwagę , że musi ona być powołana z przedrostkiem static, bez tego nie uda się zrobić przełączania. W tasku mam na razie obsługę jednego czujnika (Wire2). Po odczycie temperatury za pomocą funkcji GetTemperature(&WireDevice2), odczytane dane pakuję do kolejki :
xQueueSend(xQueueTemperatureTask, &WireDevice2, (TickType_t)0), którą wcześniej sobie powołałem w tym miejscu :


Kolejka jest 4-ro elementowa. Czyli upakuję tam cztery całe struktury o typie temperatureDevice_t . Dane z kolejki będę odczytywał w zadaniu vDisplayTask w którym zostaną wyświetlone na poszczególnych wyświetlaczach LED. W zadaniu vTemperatureTask posługuję się czymś takim jak sekcja krytyczna taskENTER_CRITICAL()taskEXIT_CRITICAL(). Jest to specjalny zestaw funkcji , którymi zabezpieczamy operację krytyczne z punktu widzenia nierozdzielności ich wykonania . W sekcji krytycznej wyłączane są przerwania przyporządkowane do RTOS, czyli wyłączany jest m.in mechanizm przełączenia kontekstu. Innymi słowy nic co RTOS-owe nie przerwie nam wykonania operacji. Ponieważ operacje blokujące na 1-Wire są czułe na poprawność czasową delay, dlatego nie możemy tych operacji przerywać innym kontekstem. Blokująca obsługa 1-Wire została wytyczona czasowo tak (2,5 s) aby nie wchodziła w drogę zadaniu vClockTask, które jest najważniejszym zadaniem w aplikacji.

Teraz zobaczymy jak dane o temperaturze są odbierane w zadaniu vClockTask. Po stronie tego zadania powołuję strukturę TemperatureDevice, w której odbierane będą dane z zadania vTemperatureTask


Cały proces odbierania danych z kolejki i wyświetlania temperatury realizowany jest poniższym kodem :

Kod muszę uzupełnić oczywiście o obsługę dla drugiego czujnika DS18B20 (Wire1). Nie wiem tylko jeszcze czy utworzę drugą kolejkę czy w tej samej kolejce będę wysyłał dane dla dwóch czujników. Ale to już szczegół techniczny. Generalnie mechanizm kolejek w RTOS jest fajny, podoba mi się to rozwiązanie i w ogóle cała zabawa z RTOS. Już teraz wiem, że pisanie aplikacji dla mojego zegarka bez użycia RTOS-a byłoby nudnym zajęciem :).
 
Pobieram dane z dwóch czujników i pakuje je do jednej kolejki w zadaniu vTemperatuteTask, którą odbieram w zadaniu vDisplayTask  :
 
 
Uprościłem kod w zadaniu vDisplayTask  przenosząc funkcjonalność wyświetlania temperatury z dwóch czujników do interfejsu obsługującego MAX7219 efekt poniżej :
 

Funkcje mam tak napisane aby obsługiwały dowolną ilość DS18B20 , każdy na oddzielnym pinie MCU. Oczywiście w pewnych okolicznościach nie jest to optymalne a bardziej zasadne jest użycie jednego pinu (jednej linii 1-Wire) i wieszanie czujników jak światełka na choińce . Nie mam jednak , żadnych szczególnych wymogów co do obsługi DS18B20 . Udało mi się nawet blokującą obsługę umieścić w RTOS tak aby nie kolidowała z najważniejszym taskiem w systemie. Nie mniej warto mieć w zanadrzu możliwość prostej i nieblokującej obsługi DS18B20 . A w tym zakresie udało mi się już napisać działającą maszynę stanów do obsługi nieblokującej 1-Wire . Jeśli uda mi się napisać obsługę funkcji wysyłających bajt i odbierających bajt z DS18B20 przy pomocy maszyny stanów, zrobię krok milowy dla ludzkości :) bo mało kto się odważył szarpnąć na takie rozwiązanie.

Pisanie aplikacji "zegarkowej" skłania mnie do pewnych refleksji dotyczących IDE. Dociera do mnie fakt, że każdy producent IDE w taki czy inny sposób ogranicza nam wolność i skłania do uzależnienia od swoich produktów . Mało tego, edytory kodu w każdym jednym znanym mi IDE nie są zbyt wygodne . Weźmy np. taki sztandarowy produkt ARM-a jak Keil uVison. Bulimy za niego jak za zboże a w zamian mam wrażenie, że dostajemy środowisko zatrzymane w czasie o 10 lat do tyłu. Producentom oprogramowania wydaje się, że jak mają dobry produkt to opakowanie się nie liczy. Zerknijmy również na możliwości portowania naszego projektu na inne IDE. O ile import projektów z Keila, IAR , Eclipsa jest możliwy o tyle w drugą stronę możemy napotkać problem. Innym aspektem jest jeszcze sprawa praw licencyjnych, które ograniczają nam możliwość pisania komercyjnych aplikacji.
 
W czym zatem pisać kod  jeśli nie w IDE producenta MCU czy w IDE płatnych takich jak Keil, IAR, SEGGER. Okazuje się, że jest takie środowisko , które bije na głowę wszelkie edytory kodu zawarte w dostępnych na rynku IDE. Jest bezpłatne, komfortowe, przyjazne dla oczu , konfigurowalne i tworzy portowalne aplikacje. Jest nim VSCode.

Projekt zegarka rozwija się. Teraz czas zająć się implementacją automatycznej regulacji jasności świecenia wyświetlaczy LED. Z MAX7219 jest to niezwykle łatwe, ponieważ mamy tutaj sprzętowy mechanizm regulacji jasności z zakresem 16 progów. Zatem regulacja jasności nie jest płynna, tylko progowa, ale to jest absolutnie wystarczający mechanizm dla mojego zegarka. Na początek upraszczam zagadnienie do bólu i typuję organoleptycznie dwa progi jasności dzień / noc. Teraz jak to fizycznie zrealizować. 

Punktem wyjściowym jest poniższy schemat :


Pomiar światła realizuje mi fotorezystor o zakresie rezystancji  10k - 1M i zależnej od padającego światła. W efekcie otrzymuję klasyczny dzielnik napięcia. Gdzie VCC - 3.3V . Policzmy sobie jakie wartości napięcia otrzymamy w punkcie ROPT dla dwóch skrajnych zakresów rezystancji fotorezystora :




Mamy zatem dla 10k - 0.3 V w punkcie ROPT i dla 1M - 3V. Czyli zakres napięcia jakie możemy zobaczyć w punkcie ROPT to (0.3 - 3) V. No fajnie, teraz czas za pomocą MCU zmierzyć wartość napięcia w punkcie ROPT i w zależności od wyniku pomiaru przełączać progi jasności świecenia w MAX7219. Oczywiście użyję do tego modułu ADC w moim MCU.

W STM32G071 mamy jeden moduł ADC i 16 kanałów zewnętrznych oraz 3 wewnętrzne i to wszystko sterowane multiplekserem.  Ja skorzystam z jednego kanału. Punkt ROPT dzielnika mam podpięty do pinu PA7 , który jest skorelowany z kanałem nr 7 modułu ADC. STM32G071 w obudowie 32-pin nie ma wejścia VREF+. Jest ono wewnętrznie podłączone do napięcia zasilania 3.3 V. Jesteśmy w tym przypadku zdani tylko na ten jeden poziom napięcia odniesienia dla ADC.
Jest jeszcze coś takiego jak VREFINT = 1.212 V i myślałem na początku , że to jest to  na co nazwa wskazuje ale jest to zmyłka. VREFINT służy tylko do pomiaru napięcia na pinie VREF+ i wylicza się to ze wzoru podanego w dokumentacji.

No dobrze zreasumujmy. Chcę mierzyć za pomocą ADC zakres napięć 0.3 - 3 V .  Przetwornik ADC ma możliwość operowania max na 12 bitach , ja ustawiam sobie 10-bit czyli 1024 ( 0....1023 ) progów przetwornika.  Jeśli podzielimy dostępne napięcie referencyjne 3.3 V przez 1024 to wyjdzie nam jaka jest minimalna wartość zmiany napięcia jaką możemy zmierzyć. Czyli dla odczytanej wartości z ADC = 1 mamy 0,00322 V (3.22 mV).
Przykładowo ADC = 501 reprezentuje nam wartość zmierzonego napięcia 1.61V

Moduł ADC od strony rejestrów może wydać się na pierwszy rzut oka przytłaczający. Ale samo ustawienie i odpalenie pomiarów za pomocą rejestrów jest trywialnie proste. Dla maksymalnego uproszczenia w realizacji pomiaru jasności w moim zegarku nie będę korzystał z żadnych przerwań, DMA etc. nie muszę nawet czytać flag o zakończonej konwersji, to jest wszystko zbędne dla efektu , który chcę uzyskać.

Aby odpalić ADC na pinie PA7 musimy włączyć zegar dla ADC a PA7 ustawić w trybie analogowym. Ten tryb jest domyślny dla pinów , więc teoretycznie nie trzeba nic w rejestry wpisywać.


Cała konfiguracja ADC i włączenie konwersji w trybie ciągłym wygląda u mnie jak poniżej :


Jak pisałem wcześniej nie korzystam z przerwań ani nie sprawdzam flag ponieważ odczytu z rejestru ADC dokonuję cyklicznie co 1.5 sekundy. Nie potrzebuję szybciej.

Do obsługi ustawiania jasności powołałem nowe zadanie RTOS-owe i nazwałem je vOptoResitorsTask. Cała obsługa ustawiania jasności w moim zadaniu wygląda jak poniżej :


Prościej już się chyba nie da. Na razie posługuję się dwoma progami jasności (7 i 15), docelowo pewnie zwiększę ilość progów. I o dziwo to działa nawet :). Do wygodnego sprawdzenia czy ADC generuje dane użyłem semihostingu. 

Na razie RTOS daje radę przełknąć moje szaleństwa i jest git. Cały czas mam odczucia pozytywne co do RTOS-a , aplikacja się nie sypie ,wszystko działa zgodnie z oczekiwaniem. Rozbijanie aplikacji na zadania jako oddzielne byty w wielu aspektach upraszcza pisanie aplikacji w stosunku do podejścia  bez RTOS. W sumie jestem zadowolony z tego co dokonałem przy użyciu RTOS-a.

W międzyczasie zmieniłem trochę projekt płytki zegara i dodałem w nim dodatkowe złącze do programowania tak aby można było z innych programatorów korzystać nie tylko J-Linka . Dodałem sygnalizator dźwiękowy tak na wszelki wypadek jakbym poczuł potrzebę implementacji budzika. Pogrubiłem ścieżki do paneli dotykowych i dokonałem innych drobnych zmian.

Do przedniego panelu zegarka zamówiłem dymione na grafitowo pleksi o grubości 8 mm. Efekt wyświetlania powinien być "zajefajny".
 
Mój zegarek jest doskonałym poligonem doświadczalnym dla nauki programowania a w szczególności do nauki RTOS.

Jestem obecnie na etapie lutowania nowej poprawionej płytki. Aczkolwiek jeszcze będę ja delikatnie modyfikował , w szczególności chcę zaryzykować zmianę kolorystyki na fioletowy. Czarny kolor już trochę mi się opatrzył. Składam zegarek, który będę już mógł zaprezentować rodzinie. W międzyczasie rozejrzałem się za modułami radiowymi i zamówiłem moduły LORA na nowym chipie LLCC68. Mam co prawda w odwodzie moduły SI4463 ale są one już trudno dostępne. Moduł radiowy wykorzystam w zegarku m.in do synchronizacji czasu i do komunikacji z chmurą . Zegarek będzie łączył się z modułem opartym o Wiznet 5500 + RF, moduł ten będzie podpięty kablem Ethernet bezpośrednio do routera WiFi. Do takiego sposobu synchronizacji czasu nie będzie potrzebna żadna konfiguracja sieci od strony zegarków.

Z rzeczy ciekawych to ostatnio zakupiłem sobie polski pogramator/debugger ZL33PRG –  CMSIS-DAP . Zaprogramujemy nim dowolnego ARM-a nie tylko STM32 . Posłużę się nim w projektach , które mam zamiar stawiać w VSCode. Jego zaletą oprócz ceny, jest to, że nie ma żadnych ograniczeń licencyjnych tak jak np. J-Link EDU Mini

Odnośnie budzika jaki dodałem do swojego zegarka na zaktualizowanej  płytce. W pierwotnej wersji nie wpadłem na ten pomysł aby mieć w zegarku budzik :). Rzecz niby trywialna ale jest jeden niuans w sterowaniu budzikiem. Najpierw schemat implementacji :

Wykorzystałem tutaj buzzer z generatorem w wersji 5V, tani i łatwo dostępny. Tranzystor pnp sterowany z wybranego pinu MCU. W przypadku próby sterowania pinem ustawionym w standardowym trybie push-pull ,nie uda nam się wyłączyć buzzera. Prawidłowe ustawienie pinu to open-drain , tylko w tym trybie uda nam się poprawnie sterować buzzerem . Buzzer'a nie aktywujemy w trybie ciągłego działania bo inaczej zwariujemy, tylko przerwana praca raz na 1 s lub dłużej.

Działająca wersja prototypowa zegarka wygląda na chwilę obecną jak poniżej :
 



W realu wygląda lepiej niż na zdjęciach. Prototyp zaprezentowałem rodzinie no i stałem się bohaterem dnia :). Najbardziej zależało mi na opinii mojego Syna nastolatka, cytując jego słowa " Tata , odjazdowy zegarek ". Dymione pleksi robi robotę, zegarek prezentuje się dzięki niemu bardzo elegancko i dodaje subtelności wyświetlanym cyfrom. Muszę jeszcze dopracować szczegóły estetyczne związane ze "śrubunkiem". O ile w pierwszej wersji płytki , jedno pole dotykowe było trochę "leniwe" o tyle w obecnej wersji ver 3.x, dotyk śmiga bez zająknięcia. Z dotyku jestem bardzo zadowolony , szybciej się na nim wykonuje operacje niż na mechanicznych przyciskach i odpada problem drgania styków. Aby operować dotykiem , zegarek stawiamy na tylne nóżki i palcem swobodnie operujemy pod przednim pleksi.



Pozdrawiam

PICmajster
picmajster.blog@gmail.com

Brak komentarzy:

Prześlij komentarz