Skuterowy starter, kod
Założenia
Jest to mój pierwszy projekt na mikrokontroler, a do tego nigdy wcześniej nie używałem Zephyra, więc poznawałem go w trakcie pisania. Może być tak, że niektóre rzeczy można zrobić lepiej, więc ten projekt się sprawdził jako lekcja #embedded .
Jak powinien działać kontroler? Normalny workflow skutera wygląda (w uproszczeniu) następująco:
- Przekręć kluczyk w stacyjce,
- Odpal silnik (wciśnij hamulec + rozrusznik),
- Jedź,
- Przekręć kluczyk aby zgasić silnik.
Muszę więc zasymulować trzy kroki: włączanie zasilania, odpalanie oraz gaszenie.
Włączanie zasilania
Aby nie rozładować akumulatora, kontroler głównie zasilany jest z zewnętrznego akumulatora litowo-jonowego.
💬 Ponieważ łącznie używane są dwa akumulatory, skuterowy 12V nazywany będzie "akumulatorem", a mniejszy, zewnętrzny, litowo-jonowy,. "akumulatorkiem".
Kontroler może dawać dwa rodzaje napięcia 3.3V oraz 5V, ale 5V nie jest dostępne na zasilaniu z akumulatorka (który w najlepszym wypadku ma 3.6V). Sterowanie skuterem odbywa się przez przełączanie przekaźników podpiętych do GPIO kontrolera. Żeby włączyć zasilanie, konieczne jest przełączenie przekaźnika podłączonego równolegle do stacyjki, co udawać będzie przekręcenie kluczyka. W tym momencie kontroler przełączany jest na zasilanie z akumulatora, przez rozebrany samochodowy adapter 12V-USB, dzięki czemu pojawia się zasilanie na linii 5V. Linia 5V jest podpięta przez dzielnik napięcia do pinu ADC (ADC w Xiao BLE obsługuje max 3.6V, 5V to za dużo, więc zmniejszam je dzielnikiem o połowę), dzięki czemu możliwe jest wykrycie czy zasilanie zostało poprawnie włączone.
Odpalanie
Początkowo zakładałem użycie jednego przekaźnika, który będzie kręcił rozrusznikiem, jednak okazało się, że lepiej będzie użyć dwóch i dokładniej udawać skuter (jeden od hamulca, drugi od rozrusznika). Wymagane są więc dwa przekaźniki, ale mogą być one podpięte do jednego pinu GPIO.
Jakoś muszę być w stanie stwierdzić, że silnik pracuje. Postanowiłem użyć czujnika natężenia prądu wpiętego w przewód idący od prądnicy - jeśli leci jakiś prąd, to prawdopodobnie silnik pracuje. Oczywiście nie mogło być za łatwo, ale to już inna historia. Oznacza to użycie kolejnego pinu GPIO z funkcją ADC. W teorii użyty przeze mnie układ daje na wyjściu napięcie od 2.5V (przy 0A) do 5V (przy 20A), ale w praktyce zasilanie skuter nigdy nie przekraczało 4A, więc obejdzie się bez dzielnika napięcia.
Gaszenie
Tak jak przy odpalaniu, okazało się, że nie jest to takie proste. Aby zgasić silnik nie trzeba odciąć zasilania, ale zewrzeć dziwny przewód na którym jest 60V do masy. Z jednej strony oznacza do konieczność podpięcia kolejnego przekaźnika i kolejnego GPIO. Otwiera to jednak pewną ciekawą opcję - możliwe będzie zgaszenie silnika bez odłączania zasilania.
Kod
Biorąc pod uwagę powyższe, założenia względem kodu są następujące:
- Sterowanie przez Bluetootha
- Wymagane sparowanie urządzeń w sposób uniemożliwiający połączenie przez przypadkową osobę
- Podział kodu na
34 moduły:- QMBStarter - moduł odpowiedzialny za główną logikę startera
- Bluetooth - moduł odpowiedzialny za komunikację
- Notifications - moduł odpowiedzialny za powiadomienia o działaniu kontrolera (LED, buzzer i ekran)
- Sensors - moduł dodany później, odpowiedzialny za odczyt danych z ADC/czujników (stan baterii, prąd ładowania, ...)
- Moduły tego potrzebujące odpalone na osobnych wątkach, żeby nikt nikogo nie zablokował
Architektura
Powyższy diagram przedstawia główne moduły i ich relacje. Poniżej dokładniejszy opis poszczególnych komponentów.
main()
Punkt wejściowy programu. Jego zadanie w tym przypadku polega na kolejno:
- Zainicjalizowanie modułu Sensors
- Zainicjalizowanie modułu Notifications
- Powiadomieniu użytkownika, że kontroler wystartował, czyli wyświetlenie splasha na ekranie
- Włączenie Bluetootha w Zephyrze:
bt_enable
- Załadowanie aktualnych ustawień startera (TODO)
- Wystartowanie wątków dla modułów QMBStarter oraz Bluetooth
- Poinformowanie użytkownika, że wszystko się uruchomiło
- Sprawdzanie czy wątki dalej działają i poinformowanie o ewentualnym błędzie
QMBStarter
Moduł QMBStarter jest maszyną stanów / automatem skończonym - obiektem, który w danym momencie znajduje się w określonym stanie i - zależnie od swoich reguł - może przeskoczyć w inny. W tym przypadku dozwolone stany to:
DISABLED
- stan początkowy, po uruchomieniu kontrolera. Od razu przeskakuje naINITIALIZING
.INITIALIZING
- stan podczas którego następuje inicjalizacja wszystkich elementów modułu (np. GPIO podłączone do przekaźników). Po zakończeniu przeskakuje doPOWERED_OFF
.POWERED_OFF
- stan gdy zasilanie skutera jest wyłączone (sytuacja analogiczna do kluczyka poza stacyjką). Na żądanie przeskakuje doPOWERING_ON
.POWERING_ON
- stan w którym zasilanie skutera jest włączane. Przełączany jest przekaźnik od zasilania, a następnie moduł czeka chwilę na pojawienie się zasilania (5V). Przeskakuje naPOWERED_ON
gdy zasilanie się pojawi lubPOWERED_OFF
gdy nie.POWERED_ON
- stan gdy zasilanie skutera jest włączone (sytuacja analogiczna do przekręconego kluczyka w stacyjce). Możliwe jest odpalenie skutera przez kontroler lub 'tradycyjnie'. Może przeskoczyć na jeden z poniższych stanów:- Jeśli poproszono o odpalenie skutera:
STARTER_INIT
- Jeśli poproszono o wyłączenie zasilania:
POWERING_OFF
- Jeśli poproszono o odpalenie skutera:
POWERING_OFF
- stan gdy zasilanie skutera jest wyłączane. Rozłączany jest przekaźnik od zasilania, a następnie moduł czeka chwilę na zanik zasilania (5V). Przeskakuje naPOWERED_OFF
gdy zasilanie zniknie lubPOWERED_ON
gdy nie.STARTER_INIT
- stan inicjalizujący wszystkie rzeczy potrzebne do odpalenia skutera, czyli głównie licznik prób odpalenia. Po zakończeniu przeskakuje doENGINE_STARTING
.ENGINE_STARTING
- stan w którym następuje próba odpalenia silnika. Włączane są przekaźniki od stopu oraz rozrusznika. W trakcie działania sprawdzane jest wskazanie czujnika prądu. Jeśli jego wskazanie nie będzie sugerowało, że silnik odpalił, to przeskakuje doENGINE_START_COOLDOWN
, a w przeciwnym razie doENGINE_STARTED
.ENGINE_START_COOLDOWN
- stan dający rozrusznikowi chwilę wytchnienia po nieudanym odpaleniu. Na wypadek jakby silnik jednak odpalił, ale nie zostało to wykryte od razu, cały czas monitoruje wskazanie czujnika prądu. Po kilku sekundach zwiększa licznik prób odpalenia i przeskakuje doENGINE_STARTING
lubPOWERED_ON
jeśli liczba prób została przekroczona.ENGINE_STARTED
- stan informujący wszystkich o udanym odpaleniu oraz czyszczący zmienne używane podczas odpalania. Przeskakuje doENGINE_RUNNING
.ENGINE_RUNNING
- stan gdy silnik pracuje. Co jakiś czas sprawdza czy silnik dalej pracuje (rozważałem opcję automatycznego odpalania jakby zgasł, ale może to być niebezpieczne). Przeskakuje doENGINE_STOPPING
na żądanie zgaszenia silnika lub gdy zostanie wykryte, że sam zgasł.ENGINE_STOPPING
- stan gdy silnik jest gaszony. Zwierany jest przekaźnik od gaszenia silnika (60V do masy). Przez kilka sekund wielokrotnie sprawdza wskazanie czujnika prądu, jeśli przez dłuższy czas sugeruje ono, że silnik się zatrzymał, to przeskakuje doPOWERED_ON
lubPOWERING_OFF
, zależnie czy poproszono o zgaszenie silnika czy całkowite wyłączenie zasilania. Gdy nie uda się zgasić... raczej nie powinno się to zdarzyć, ale w takiej sytuacji przeskakuje doPOWERING_OFF
- przekaźnik gaszenia silnika jest od wtedy zwarty na stałe, więc prędzej czy później powinien się zatrzymać.
Wszystkie stany w kodzie są jako enum, więc mają przypisane konkretne wartości liczbowe.
Moduł ten ma następujący interfejs:
int qmb_service_start();
int qmb_request_power_on();
int qmb_request_engine_start();
int qmb_request_power_off();
int qmb_request_engine_stop();
Pierwsza funkcja zawiera główną pętlę modułu, która w nieskończoność aktualizuje stan maszyny (smf_run_state
). Pozostałe cztery zgłaszają prośbę o przejście do konkretnego stanu. Aby uniknąć dziwnych sytuacji, takich jak starter ciągle próbujący się włączyć, dozwolone jest przejście tylko do jednego z czterech, odpowiednio: POWERED_ON
, ENGINE_RUNNING
, POWERED_OFF
oraz POWERED_ON
. qmb_request_power_on
oraz qmb_request_engine_stop
generalnie robią to samo, ale jest kilka powodów na ich rozdzielenie. Pierwszy - czytelność. Chcemy móc włączyć zasilanie, wyłączyć zasilanie, odpalić silnik lub go zgasić, więc interfejs modułu temu odpowiada. Drugi - potencjalnie żądanie włączenia zasilania oraz zgaszenia silnika mogą mieć jakąś dodatkową logikę. Trzeci - maszyna stanów ma zdefiniowane ścieżki jakimi może się poruszać aby przejść z aktualnego stanu do innego:
static const QMBState next_state_map[QMB_STATE_COUNT][QMB_STATE_COUNT]=
{
// Next state from v towards >
// QMB_DISABLED QMB_INITIALIZING QMB_POWERED_OFF QMB_POWERING_ON QMB_POWERED_ON QMB_POWERING_OFF QMB_ENGINE_START_INIT, QMB_ENGINE_STARTING QMB_ENGINE_START_COOLDOWN QMB_ENGINE_STARTED QMB_RUNNING QMB_ENGINE_STOPPING
/*QMB_DISABLED*/ {QMB_DISABLED, QMB_INITIALIZING, QMB_INITIALIZING, QMB_INITIALIZING, QMB_INITIALIZING, QMB_INITIALIZING, QMB_INITIALIZING, QMB_INITIALIZING, QMB_INITIALIZING, QMB_INITIALIZING, QMB_INITIALIZING, QMB_INITIALIZING},
/*QMB_INITIALIZING*/ {QMB_INITIALIZING, QMB_INITIALIZING, QMB_POWERED_OFF, QMB_POWERED_OFF, QMB_POWERED_OFF, QMB_POWERED_OFF, QMB_POWERED_OFF, QMB_POWERED_OFF, QMB_POWERED_OFF, QMB_POWERED_OFF, QMB_POWERED_OFF, QMB_POWERED_OFF},
/*QMB_POWERED_OFF*/ {QMB_POWERED_OFF, QMB_POWERED_OFF, QMB_POWERED_OFF, QMB_POWERING_ON, QMB_POWERING_ON, QMB_POWERING_ON, QMB_POWERING_ON, QMB_POWERING_ON, QMB_POWERING_ON, QMB_POWERING_ON, QMB_POWERING_ON, QMB_POWERED_OFF},
/*QMB_POWERING_ON*/ {QMB_POWERED_OFF, QMB_POWERED_OFF, QMB_POWERED_OFF, QMB_POWERING_ON, QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERED_OFF},
/*QMB_POWERED_ON*/ {QMB_POWERING_OFF, QMB_POWERING_OFF, QMB_POWERING_OFF, QMB_POWERING_OFF, QMB_POWERED_ON, QMB_POWERING_OFF, QMB_ENGINE_START_INIT, QMB_ENGINE_START_INIT, QMB_ENGINE_START_INIT, QMB_ENGINE_START_INIT, QMB_ENGINE_START_INIT, QMB_POWERING_OFF},
/*QMB_POWERING_OFF*/ {QMB_POWERED_OFF, QMB_POWERED_OFF, QMB_POWERED_OFF, QMB_POWERED_OFF, QMB_POWERED_OFF, QMB_POWERING_OFF, QMB_POWERED_OFF, QMB_POWERED_OFF, QMB_POWERED_OFF, QMB_POWERED_OFF, QMB_POWERED_OFF, QMB_POWERED_OFF},
/*QMB_ENGINE_START_INIT*/ {QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERED_ON, QMB_ENGINE_START_INIT, QMB_ENGINE_STARTING, QMB_ENGINE_STARTING, QMB_ENGINE_STARTING, QMB_ENGINE_STARTING, QMB_POWERED_ON},
/*QMB_ENGINE_STARTING*/ {QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERED_ON, QMB_ENGINE_STARTING, QMB_ENGINE_START_COOLDOWN, QMB_ENGINE_STARTED, QMB_ENGINE_STARTED, QMB_POWERED_ON},
/*QMB_ENGINE_START_COOLDOWN*/ {QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERING_OFF, QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERED_ON, QMB_ENGINE_STARTING, QMB_ENGINE_START_COOLDOWN, QMB_ENGINE_STARTING, QMB_ENGINE_STARTING, QMB_POWERED_ON},
/*QMB_ENGINE_STARTED*/ {QMB_RUNNING, QMB_RUNNING, QMB_RUNNING, QMB_RUNNING, QMB_RUNNING, QMB_RUNNING, QMB_RUNNING, QMB_RUNNING, QMB_RUNNING, QMB_ENGINE_STARTED, QMB_RUNNING, QMB_RUNNING},
/*QMB_RUNNING*/ {QMB_ENGINE_STOPPING, QMB_ENGINE_STOPPING, QMB_ENGINE_STOPPING, QMB_ENGINE_STOPPING, QMB_ENGINE_STOPPING, QMB_ENGINE_STOPPING, QMB_ENGINE_STOPPING, QMB_ENGINE_STOPPING, QMB_ENGINE_STOPPING, QMB_ENGINE_STOPPING, QMB_RUNNING, QMB_ENGINE_STOPPING},
/*QMB_ENGINE_STOPPING*/ {QMB_POWERING_OFF, QMB_POWERING_OFF, QMB_POWERING_OFF, QMB_POWERING_OFF, QMB_POWERED_ON, QMB_POWERING_OFF, QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERED_ON, QMB_POWERED_ON, QMB_ENGINE_STOPPING},
};
Teoretycznie funkcja qmb_request_engine_stop
mogłaby zgłaszać prośbę o przejście do stanu ENGINE_STOPPING
, ale by to zrobić, silnik by musiał być najpierw odpalony. Przejście z ENGINE_RUNNING
do POWERED_ON
zahacza o ENGINE_STOPPING
, więc żądanie przejścia do POWERED_ON
podczas gaszenia likwiduje sytuację, że silnik zostanie odpalony i natychmiast zgaszony.
Bluetooth
Urządzenie Bluetooth Low Energy oferuje pewne usługi (service), z których każda ma kilka charakterystyk (characteristic). Mój kontroler oferuje trzy usługi:
- QMBStarter Service - usługa odpowiedzialna za przyjmowanie żądań zmiany stanu startera oraz informująca o jego aktualnym stanie,
- Sensor Service - usługa udostępniająca aktualne wskazania czujników (obecność 5V oraz prąd ładowania), do celów debugowych,
- Configuration Service - usługa pozwalająca na zmianę ustawień startera, takich jak czas kręcenia rozrusznikiem lub liczba prób odpalenia.
QMBStarter Service
Posiada dwie charakterystyki: żądanie zmiany stanu (request
), tylko do zapisu, oraz aktualny stan startera (state
), tylko do odczytu, z opcją notyfikacji (automatycznego informowania klienta o zmianie, zamiast ręcznego wysyłania wartości).
Charakterystyka request
oczekuje wysłania do niej wartości liczbowej odpowiadającej stanowi do którego moduł QMBStarter powinien przejść: QMB_POWERED_OFF
, QMB_POWERED_ON
, QMB_RUNNING
oraz QMB_ENGINE_STOPPING
. Wartości te mają logicznie odpowiadać oczekiwanej akcji, dlatego żądanie zgaszenia silnika wysyła ENGINE_STOPPING
- moduł QMBStarter wewnętrznie zamienia sobie to na POWERED_ON
.
Sensor Service
Również posiada dwie charakterystyki: informację o dostępnie do napięcia 5V oraz wskazanie czujnika natężenia prądu. Wskazania mogą zmieniać się w dowolnym momencie, więc wartości wysyłane są co pewien czas.
Configuration Service
Daje dostęp do konfiguracji modułu. Każda charakterystyka to jedna z wartości którą można zmienić. Po zmianie informuje moduł QMBStarter o nowych wytycznych.
Łączenie i parowanie
Po połączeniu (lub rozłączeniu) z kontrolerem, moduł Bluetooth informuje moduł Notifications o nawiązaniu (lub utraceniu) połączania.
Zephyr oferuje kilka metod sparowania urządzeń, między innymi:
- Wyświetlenie kodu parowania, do wpisania na kliencie,
- Wyświetlenie kodu parowania na kliencie, do wpisania tutaj,
- Potwierdzenie zgodności kodu na obu urządzeniach.
Z racji braku klawiatury, opcje 2. i 3. zostały odrzucone. Opcja 1. wydawała się dobra: kod parowania zostanie wyświetlony na ekranie, który nie będzie publicznie widoczny. Oznacza to, że aby sparować telefon z kontrolerem, wymagany będzie fizyczny dostęp do niego, a jak ktoś go będzie miał, to równie dobrze może odpalić skuter zwierając przewody i ukraść go w ten sposób.
Sensors
Moduł odpowiedzialny za inicjalizację ADC oraz przetwarzanie wartości przychodzących z czujników. Udostępnia trzy informacje: dostępność napięcia 5V, prąd ładowania oraz informacje o akumulatorku.
bool sensor_is_battery_connected();
bool sensor_is_battery_charging();
int sensor_get_battery_percentage();
bool sensor_is_5V_connected();
int sensor_get_charging_current();
Sprawdzanie dostępności napięcia 5V odbywa się przez odczytanie wartości z ADC podpiętego przez dzielnik napięcia do linii 5V i porównanie jej z pewną wartością graniczną. Z powodu dzielnika, ADC może wskazać wartości między 0V a 2.5V, uznaję więc, że wszystko powyżej 2V oznacza, że 5V jest dostępne.
Odczyt natężenia prądu ładowania wymaga trochę więcej zabawy. Czujnik prądu na wyjściu daje wartości w przedziale \([{V_{ref} \over 2}, V_{ref}]\), 100mV na każdy amper płynącego prądu. \(V_{ref}\) to napięcie zasilania czujnika, czyli w moim wypadku 5V. Oznacza to, że czujnik wskazuje:
Natężenie | Napięcie na wyjściu |
---|---|
0A | 2.5V |
1A | 2.6V |
2A | 2.7V |
... | ... |
18A | 4.3V |
19A | 4.4V |
20A | 4.5V |
Zwracana wartość natężenia prądu obliczana jest więc ze wzoru: \( I = (V_{ADC} - 2.5V) * 0.1{A \over V} \) lub kodem:
return (v_ADC - 2500) * 10; // ADC daje wartości w mV, funkcja zwraca w mA
Bateria
Xiao BLE posiada styki do podłączenia akumulatorka oraz wbudowany moduł do ładowania go, który udostępnia informacje o stopniu naładowania. Dla ułatwienia, postanowiłem użyć biblioteki do jego obsługi.
Na ekranie podłączonym do kontrolera chciałem wyświetlać ikonkę zależną od stanu zasilania:
- Zasilanie zewnętrzne,
- Ładowanie akumulatorka,
- Zasilanie z akumulatorka.
Używana przeze mnie biblioteka udostępnia informacje o tym czy akumulatorek jest ładowany, ale nie o tym czy jest w ogóle podpięty. Wiem jednak, że uznawany jest za "ładowany" tylko wtedy, gdy dostępne jest napięcie 5V - nawet gdy akumulatorek dojdzie do 100%. Mając te informacje, są tylko 4 możliwości:
Czy akumulatorek jest podpięty? | Czemu tak sądzisz? | |
---|---|---|
5V + ładowanie | Tak | Jeśli akumulatorek jest ładowany, to musi być podpięty. |
Ładowanie | Tak | Jeśli akumulatorek jest ładowany, to musi być podpięty... pytanie tylko - co go ładuje? Raczej niemożliwa sytuacja. |
5V | Nie | 5V to zewnętrzne źródło zasilania. Jeśli jest podłączone, ale akumulatorek nie jest ładowany, to znaczy, że go nie ma. |
Nic | Tak | Jeśli nie ma zewnętrznego zasilania, ale kontroler działa, to musi to być zasługa akumulatorka. |
Notifications
Moduł odpowiedzialny za informowanie użytkowników o aktualnym stanie startera. Aktualnie mam dostęp do trzech komponentów:
- Kolorowy wyświetlacz OLED, 96x64 pikseli,
- Buzzer jednotonowy,
- 3 LEDy: zielony, czerwony i niebieski.
Każdy z nich ma swój własny timer który decyduje kiedy ma być kolejna aktualizacja jego stanu. Dokładny czas na jaki zostanie odpalony timer zależny jest od ostatniego wydarzenia. Jeśli aktualnie silnik jest odpalany, to timery będą odpalane częściej, niż gdy skuter jest wyłączony. Dodatkowo, uruchomiony jest timer który ma co minutę zamrugać diodą - informacja o tym, że kontroler się nie zawiesił. Callback każdego timera puszcza nowe zadanie do systemowego work queue. Zdefiniowane są cztery zadania: aktualizacja wyświetlacza, buzzera, LEDów oraz wszystkiego na raz. Użyłem work queue aby nie - między innymi - dopuścić do sytuacji, że wyświetlacz jest aktualizowany przez kilka źródeł na raz.
Moduł Notifications ma kilka publicznie dostępnych funkcji które są wywoływane przez inne moduły gdy coś się stanie, na przykład ktoś połączy się przez Bluetootha:
void notify_initializing();
void notify_ready();
void notify_error();
void notify_bt_ready();
void notify_bt_pairing(bool is_pairing, int pairing_code);
void notify_bt_pairing_finished(bool paired);
void notify_bt_connected(bool is_connected);
void notify_qmb_state_changed(QMBState state);
void notify_qmb_power_not_turned_on();
void notify_qmb_engine_not_started();
void notify_battery_charging_state_changed(bool is_charging);
void notify_battery_charge_changed(int percentage);
Każda z nich wrzuca Event
do bufora cyklicznego i wrzuca do work queue zadanie zaktualizowania wszystkiego. Zadanie to ściąga eventy po kolei z bufora i wywołuje funkcje aktualizujące stan wyświetlacza, buzzera i ledów, przekazując do nich aktualnie przetwarzany event. One z kolei mogą już same zdecydować kiedy chcą się zaktualizować po raz kolejny (np. aby na wyświetlaczu pokazać powiadomienie o połączeniu, które samo zniknie po kilku sekundach).
LEDy
Funkcja aktualizująca LEDy jest na tyle prosta, że dobrze pokazuje logikę stojącą za takim użyciem timerów i work queue:
static void ntf_update_leds(Event* event)
{
++ntf_led_data.blink_counter; // Licznik, do mrugania diodą
// W razie błędu...
if(ntf_data.is_error)
{
// ...zamrugaj czerwoną diodą...
gpio_pin_set_dt(&ledR, ntf_led_data.blink_counter%2);
gpio_pin_set_dt(&ledG, 0);
gpio_pin_set_dt(&ledB, 0);
// ...i rób to szybko, ale tylko pierwsze 100 razy...
if(ntf_led_data.blink_counter<100)
{
k_timer_start(&ntf_led_timer, K_MSEC(200), K_NO_WAIT);
}
// ...bo potem wystarczy mrugać raz na 2 sekundy.
else
{
if(ntf_led_data.blink_counter%2==0)
k_timer_start(&ntf_led_timer, K_MSEC(1950), K_NO_WAIT);
else
k_timer_start(&ntf_led_timer, K_MSEC(50), K_NO_WAIT);
}
return;
}
// Jeśli kontroler się dopiero inicjalizuje, to szybko mrugaj zieloną diodą.
if(ntf_data.is_initializing)
{
gpio_pin_set_dt(&ledR, 0);
gpio_pin_set_dt(&ledG, ntf_led_data.blink_counter%2);
gpio_pin_set_dt(&ledB, 0);
k_timer_start(&ntf_led_timer, K_MSEC(200), K_NO_WAIT);
return;
}
// ...
Wyświetlacz
Zephyr oferuje różne fajne narzędzia do obsługi wyświetlaczy, ale pod jednym warunkiem: wymagany jest sterownik do niego. Tak się składa, że do mojego wyświetlacza - kontroler SSD1331 - sterownika nie ma. Musiałem więc wziąć dokumentację w rękę i samemu ogarnąć jak go używać. Na szczęście Zephyr posiada sterownik do podobnych wyświetlaczy, miałem więc od czego odgapić. Jest to jednak temat na osobny wpis.
Na wyświetlaczu rysowana jest tapeta (bo czemu nie), pasek stanu z ikonami pokazującymi aktualny stan, dokładniejsze informacje o aktualnym stanie, oraz - okazjonalnie - pełnoekranowe powiadomienie o jakimś wydarzeniu (np. połączenie, parowanie, ...). Powyższy obrazek jest tylko wizualizacją, w rzeczywistości zawartość wyświetlacza może wyglądać trochę inaczej, nawet pomijając fakt, że obsługuje tylko 16 bitów koloru. Bardziej realistyczny przykład można obejrzeć na Youtube:
Obrazy, ikony i fonty
Ponieważ nie da się łatwo wgrać plików na mikrokontroler, każdy obraz i ikonka została przekonwertowana do tablicy 32-bitowych wartości oraz struktury Image
która przechowuje informacje o obrazie.
struct Image
{
const uint32_t *data;
int width;
int height;
};
Obrazy są przechowywane w formacie 32-bitowym i konwertowane do natywnego formatu wyświetlacza (w tym przypadku 16 bitowy RGB565) w momencie rysowania.
Fonty zostały potraktowane podobnie, ale dodatkowo są dwie struktury opisujące font i poszczególne znaki:
struct FontCharacter
{
// ASCII character code
int value;
// Image coordinates
int x;
int y;
int width;
int height;
};
struct Font
{
const struct Image* image;
int size;
size_t character_count;
const struct FontCharacter* characters;
};
Dla zabawy dałem sobie możliwość używania półprzeźroczystości, nakładania koloru na rysowany tekst i obrazek oraz narysowania napisu z obrysem w dowolnym kolorze. Rysowanie odbywa się do bufora, który następnie wysyłany jest do wyświetlacza. Całość działa całkiem sprawnie, ale widać wyraźnie, że nie stawiałem sobie jak najwyższej wydajności za cel ( ͡° ͜ʖ ͡°)
Komentarze