Skocz do zawartości

Elvis

Użytkownicy
  • Zawartość

    2537
  • Rejestracja

  • Ostatnio

  • Wygrane dni

    187

Wszystko napisane przez Elvis

  1. Proponuję nie zakładać nic ponad to, że ktoś szuka darmowej informacji kto i za ile taki projekt chciałby wykonać. Jak dla mnie tekst: "Budżet 10000zł. Jeśli nierealny proszę składać także wyższe oferty." oznacza, że to nie jest poważna oferta. Przecież nikt podając budżet nie zakłada, że szuka kogoś za więcej... cytując jak sądzę autora tej oferty "Gdzie sens? Gdzie logika?".
  2. Nigdzie i nikogo nie hejtowalem, chyba pod hejt znacznie bardziej pasuje zarzucanie komuś braku umiejętności. Jedyne co napisałem to ostrzeżenie, bo moim zdaniem w przypadku takich "anonimowych" ogłoszeń trzeba być ostrożnym. Szkoda czasu np. na pisanie oferty skoro ktoś przykładowo chce udowodnić że projekt powinien kosztować daną kwotę, zrobić badanie rynku, albo po prostu zyskać trochę rozgłosu w sieci.
  3. @atMegaTona nie słyszałeś nigdy o ogłoszeniach dawanych zupełnie nie po to żeby wykonać projekt? O tym właśnie pisałem odnośnie ostrożności - bo wcale nie jestem pewien, czy to ogłoszenie faktycznie ma na celu znalezienie wykonawcy, czy np. sprawdzenie za ile takie projekty są wykonywane, albo po prostu zrobienie komuś reklamy. Oczywiście mogę się mylić, ale jakoś zakres wymagań idealnie pokrywa się z tym co w EP i EdW opisuje jeden z zablokowanych użytkowników tego i innych forów... Więc jak dla mnie to nie ma żadnego budżetu, żadnego projektu, jest ogłoszenie, na które wielu naiwnych może się nabrać.
  4. A ja bym proponował mocno zastanowić się nad wiarygodnością "zlecenia" w tym temacie. Po pierwsze, ktoś sobie zadał sporo trudu, żeby napisać prawie dwie strony "wymagań". Wygląda to profesjonalnie, wreszcie jakieś poważnie wyglądające ogłoszenie - ale autor się nie podpisał. Bo chyba Fast Rabbit to nie imię i nazwisko, ani nazwa firmy. Więc jeśli ktoś spędza sporo czasu pisząc "ofertę", ale się nie podpisuje to istotne ostrzeżenie. Bo jeśli nie chce podawać swoich danych, to czego się obawia? Druga sprawa to same "założenia" projektu. Nawet po szybkim przeglądzie widać, że nic się tutaj nie zgadza. O samym działaniu rządzenia praktycznie brak informacji - 230VAC, termostat i 3 przekaźniki 8A to właściwie cały opis części wykonawczej... Natomiast o niepotrzebnych wodotryskach jest całe mnóstwo. Po co komu aż 3 wyświetlacze: oled, tft oraz matryca LED? Dlaczego aż tyle i dlaczego akurat 128x160 pikseli? to już 480x272, albo 128x128 będzie złe? Standard MISRA, który ma mieć jakieś związki z zawieszaniem... itd. itp. Prawdę mówiąc po przeczytaniu założeń mam nieodparte wrażenie, że pisał je ktoś związany z EdW i EP, kto ma opisane moduły pod ręką i z jakiegoś powodu chce z nich ulepić projekt... Mogę się oczywiście mylić, ale radziłbym ewentualnym zleceniobiorcom daleko idącą ostrożność - a moderatorom uwagę.
  5. @Harnas dziękuję za uwagi. Odczyt z plików, jak i samo przygotowanie danych jest oczywiście zrobione absolutnie nieprofesjonalnie. To co opisałem powstawało jako "dłubanina" metodą prób i błędów, a moim celem było uruchomienie przykładu z X-CUBE-AI, planowałem później wszystko zrobić od początku, ładniej, lepiej, i poprawniej oraz z ciekawszym czujnikiem (chciałem użyć czujnik gestów https://botland.com.pl/pl/czujniki-gestow/3065-apds-9960-czujnik-rgb-i-wykrywacz-gestow-33v-i2c-sparkfun-sen-12787.html). Plan był dobry, ale zabrakło czasu, więc opisałem po prostu to co udało mi się zrobić, za niedoskonałości bardzo przepraszam.
  6. W poprzednich częściach udało się zebrać dane treningowe oraz przygotować sieć neuronową. Zostało ostatnie i najprzyjemniejsze, czyli napisanie końcowego programu. Jako punkt wyjścia używam program w wersji użytej do zbierania danych. Jest w nim już praktycznie wszystko co potrzebne, czyli konfiguracja modułów peryferyjnych oraz odczyt danych z akcelerometru. Spis treści serii artykułów: Sztuczna inteligencja na STM32, czyli przykład użycia X-CUBE-AI cz.1 Sztuczna inteligencja na STM32, czyli przykład użycia X-CUBE-AI cz.2 Sztuczna inteligencja na STM32, czyli przykład użycia X-CUBE-AI cz.3 Przechodzimy do widoku CubeMX i odnajdujemy wyjątkowo kiepsko widoczny przycisk "Additional Software": Po jego naciśnięciu zobaczymy okno pozwalające na dodawanie modułów do naszego projektu. Nas interesuje moduł "STMicroelectronics.X-CUBE-AI". Jeśli wcześniej tego nie zrobiliśmy, musimy najpierw ten moduł pobrać i zainstalować. Niezbędne opcje zobaczymy w prawej części okna: Po instalacji modułu X-CUBE-AI wystarczy zaznaczyć checkbox i użyć podstawową funkcjonalność, czyli Core. Gdybyśmy planowali wykorzystanie sieci neuronowej w poważniejszym programie, takie ustawienie byłoby chyba najlepszą opcją. Jednak na potrzeby nauki możemy wykorzystać gotowiec i dodać jeszcze opcję "Application Template". Jakość kodu, który zostanie dodany jako "template" jest delikatnie mówiąc niezbyt imponująca. Dlatego napisałem, że lepiej chyba nie używać go w poważniejszych zastosowaniach - ale do nauki to bardzo wygodne rozwiązanie. Teraz możemy wrócić do głównego widoku CubeMX i odszukać moduł "STMicroelectronics.X-CUBE-AI" w kategorii "Additional Software": W środkowej części okna widzimy konfigurację modułu naciskamy przycisk-zakładkę z symbolem "+" i dodajemy naszą sieć. Domyślna nazwa to "network", ale warto ją zmienić na coś bardziej opisowego, ja wybrałem "forbot_ai". Następnie wybieramy typ modelu "Keras" oraz sam model, czyli nasz plik data2.h5 utworzony w poprzedniej części. Gdy wszystko jest już gotowe możemy nacisnąć przycisk "Analyze" i poczekać aż X-CUBE-AI sprawdzi, czy nasza sieć pasuje do wybranego mikrokontrolera. Wynik powinien wyglądać następująco: Jak widzimy cała skomplikowana sieć wykorzystuje 8.39KB pamięci flash oraz 72 bajty RAM. To bardzo mało jak dla STM32L475, ale nawet znacznie słabiej wyposażone mikrokontrolery mogą skorzystać z możliwości jakie dają sieci neuronowe. Natomiast w przypadku większych sieci możemy wykorzystać opcję kompresji. Po zapisaniu projektu i wygenerowaniu kodu zobaczymy dwie ważne zmiany w pliku main.c. Przed pętlą główną pojawi się wywołanie MX_X_CUBE_AI_Init(), a w samej pętli znajdziemy MX_X_CUBE_AI_Process(). Inicjalizacja nie będzie nas interesować i możemy zostawić wygenerowany kod bez zmian. Warto natomiast zapoznać się z treścią funkcji MX_X_CUBE_AI_Process(). Kod, który w niej znajdziemy może przyprawić o palpitacje serca, ale na szczęście to tylko przykład, chociaż chyba niezbyt udany. Możemy więc spróbować zrozumieć o co w tym kodzie chodzi - i szybko odkryjemy, że jedyne co istotne to wywołanie funkcji aiRun(), która przyjmuje dwa parametry: pierwszy to tablica z danymi wejściowymi dla sieci, a w drugiej pojawią się wyniki. Warto jeszcze odszukać jakie są wymiary tych tablic: AI_FORBOT_AI_IN_1_SIZE ma wartość 128, i jest to wielkość zdefiniowana podczas trenowania sieci, właśnie tyle danych musimy podać na wejście. Wielkość danych wynikowych znajdziemy w stałej AI_FORBOT_AI_OUT_1_SIZE, która ma wartość 2. Przykładowy kod wykonuje mnóstwo dziwnych operacji na typach danych, ale w rzeczywistości dane wejściowe i wyjściowe to zwykłe float-y. Możemy więc skasować wszystko co automat nam wygenerował i napisać pierwszy kod: void MX_X_CUBE_AI_Process(void) { /* USER CODE BEGIN 1 */ float nn_input[AI_FORBOT_AI_IN_1_SIZE]; float nn_output[AI_FORBOT_AI_OUT_1_SIZE]; aiRun(nn_input, nn_output); /* USER CODE END 1 */ } Taki program oczywiście nic nie robi, ani nie działa poprawnie, ale umieściłem go aby pokazać jak łatwe jest używanie sieci neuronowej. Musimy wypełnić tablicę nn_input danymi do przetworzenia, następnie wywołujemy aiRun() i w nn_output mamy wyniki. Poniżej wstawiam już pełny kod: static uint32_t next_ms = 1000; static float nn_input[AI_FORBOT_AI_IN_1_SIZE]; float nn_output[AI_FORBOT_AI_OUT_1_SIZE]; while (HAL_GetTick() < next_ms) {} next_ms += READ_DELAY_MS; lsm6_value_t acc; acc = lsm6_read_acc(); for (int i = 0; i < AI_FORBOT_AI_IN_1_SIZE - 1; i++) nn_input[i] = nn_input[i + 1]; nn_input[AI_FORBOT_AI_IN_1_SIZE - 1] = sqrt(acc.x * acc.x + acc.y * acc.y + acc.z * acc.z); aiRun(nn_input, nn_output); if (nn_output[0] >= 0.5f) HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET); else HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_SET); Tablica nn_input jest definiowana jako statyczna, aby zachować historię pomiarów. Pomiary są wykonywane co 20ms, stąd czekanie w pętli while. Następnie odczytywana jest wartość przyspieszeń z akcelerometru. Wyniki przechowywane w tablicy nn_input są przesuwane o jedną pozycję, a najnowszy pomiar dodawany jest na końcu. Następnie wywoływana jest funkcja aiRun() i przetwarzane są wyniki. nn_ouput zawiera dwie wartości - pierwsza pozycja to prawdopodobieństwo, że nie wykryto gestu, druga to że wykryto - ich suma wynosi 1, więc wystarczyłaby jedna wartość, ale tak była trenowana sieć. Na podstawie obliczonego prawdopodobieństwa gaszona lub zapalana jest dioda LED. Poniżej efekt działania program (z drobną poprawką o której za moment): Uczenie sieci wymaga dużej ilości danych treningowych, w sumie im więcej tym lepiej. Niestety przykładowa sieć była trenowana na bardzo małej próbce. Efekty były i tak bardzo dobre, ale po wykryciu gestu pojawiały się pewne "zakłócenia". Po pierwsze czas zapalenia diody był bardzo różny, po drugie czasem dioda zapalała się kilka razy. Oczywiście najlepiej byłoby spędzić więcej czasu ucząc sieć, ale ja zastosowałem pewne obejście. W programie, po wykryciu machania płytką dioda jest zapalana na 1s - sieć mogłaby to robić sama, ale o wiele łatwiej było zmienić program. Podsumowanie Projekt opisany w tych artykułach miał być jedynie pierwszym krokiem, taką próbą użycia X-CUBE-AI. Planowałem poprawić opisywane przykłady, dodać bardziej rozbudowany projekt z innym czujnikiem oraz wykorzystać bardziej zaawansowaną topologię sieci. Ale jak to z planami często bywa, zostały niezrealizowane. Mam jedna nadzieję, że opisane możliwości zachęcą czytelników do eksperymentowania ze sztuczną inteligencją w systemach wbudowanych oraz dzielenia się swoimi doświadczeniami. Spis treści serii artykułów: Sztuczna inteligencja na STM32, czyli przykład użycia X-CUBE-AI cz.1 Sztuczna inteligencja na STM32, czyli przykład użycia X-CUBE-AI cz.2 Sztuczna inteligencja na STM32, czyli przykład użycia X-CUBE-AI cz.3
  7. W poprzedniej części udało mi się zebrać dane treningowe, czas te dane wykorzystać oraz przygotować własną sieć neuronową. X-CUBE-AI pozwala na wykorzystanie sieci zapisanych za pomocą różnych bibliotek, zdecydowałem się wykorzystać Keras, która jest nie tylko bardzo łatwa w użyciu, ale jeszcze całkiem dobrze opisana. Wydawnictwo Helion ma o ofercie świetną książkę pt. "Deep Learning. Praca z językiem Python i biblioteką Keras", którą bardzo polecam i z której korzystałem intensywnie przygotowując ten artykuł. Spis treści serii artykułów: Sztuczna inteligencja na STM32, czyli przykład użycia X-CUBE-AI cz.1 Sztuczna inteligencja na STM32, czyli przykład użycia X-CUBE-AI cz.2 Sztuczna inteligencja na STM32, czyli przykład użycia X-CUBE-AI cz.3 Podstawą biblioteki Keras, jak i chyba większości innych związanych z AI jest język Python, więc poznanie chociaż jego podstaw jest niestety niezbędne. Kolejny brakujący element to zainstalowanie interpretera języka Python3 oraz potrzebnych bibliotek. Znacznym ułatwieniem jest wykorzystani gotowej "dystrybucji" o nazwie Anaconda. Ze strony projektu wystarczy pobrać program instalacyjny i zainstalować. Za jednym zamachem uzyskamy dostęp do języka Python3 oraz mnóstwa narzędzi pomocnych przy przetwarzaniu danych. Domyślna instalacja nie zawiera potrzebnych nam bibliotek, warto więc uruchomić program Anaconda Navigator oraz doinstalować pakiety: keras, tensorflow oraz matplotlib. Skrypty używane do trenowania sieci możemy pisać w dowolnym edytorze tekstu, a następnie uruchamiać jak każdy inny program w języku Python3, ale jest to dość niewygodne. Znacznie wygodniejszym i polecanym w wielu książkach rozwiązaniem jest wykorzystanie edytora Jupyter, albo jego nowszego kuzyna JupyterLab. W pakiecie Anaconda znajdziemy obie aplikacje. Wystarczy uruchomić wspomniany Anaconda Navigator, a w nim JupyterLab: Sam JupyterLab działa wewnątrz przeglądarki internetowej, co jest o tyle wygodne, że pozwala łatwo pracować na zdalnym komputerze - nie zawsze mamy bezpośredni dostęp do odpowiednio wydajnej maszyny, a pracując w narzędziach Jupyter możemy wykorzystać moc zdalnej jednostki. Co więcej możemy tak samo pracować na płytce typu Jetson Nano, jak i na potężnej stacji roboczej w chmurze. Zanim przejdziemy dalej warto nieco bliżej zapoznać się z zebranymi danymi. Napiszmy w pythonie prosty skrypt, który wczyta oraz przedstawi graficznie pobrane dane: Już na pierwszy rzut oka widać, że dane nie są tak łatwe do analizy jak nieco wyidealizowany przykład z wykrywaniem uderzeń serca. Na szczęście użycie sieci neuronowej zwalnia nas z obowiązku pisania programu, który będzie musiał te dane ręcznie przetwarzać. Jak pamiętamy, dane z akcelerometru to 3 liczby, które reprezentują wartości przyspieszenia względem osi XYZ. W przypadku rozpoznawania gestów możemy nieco uprościć sobie zadanie i obliczyć wypadkowe przyspieszenie. Jedna liczba to nie tylko mniej danych, ale i o wiele łatwiejsza prezentacja diagramów. Oczywiście tworząc bardziej zaawansowane projekty możemy wykorzystywać wszystkie dane. W każdym razie na wykresie widzimy jak wyglądają nasze dane źródłowe oraz momenty gdy podczas uczenia naciskany był przycisk. Poniżej jeszcze kilka przykładów kiedy następowało wykrycie samego gestu: Jak pamiętamy pomiary wykonujemy co 20 ms, ale jeden wynik nie wystarczy do stwierdzenia, czy nastąpiło wykrycie poszukiwanego gestu. Musimy więc pamiętać "historię" pomiarów. Jak pisałem w poprzedniej części, wybrałem pomiary co 20ms oraz wielkość okna pomiarowego 128 wyników. Więc nasza sieć na wejściu będzie dostawała ostatnie 128 pomiarów i ma stwierdzić - czy nastąpiło wówczas poszukiwane machanie płytką, czy nie. Teraz możemy utworzyć nowy projekt projekt w JupyterLab i zacząć trenować naszą bazę. W JupyterLab nasz skrypt można podzielić na fragmenty, edytować i uruchamiać niezależnie. Dzięki temu jeśli np. dostroimy parametry sieci, nie musimy uruchamiać całego skryptu - wystarczy ponownie wykonać zmienioną część. To ogromne ułatwienie oraz oszczędność czasu. Na początku mamy więc import używanych bibliotek: import math import numpy as np from keras import models from keras import layers from keras.utils import to_categorical Następnie podobnie jak poprzednio wczytanie naszych danych treningowych oraz wykonania niezbędnych obliczeń: f = open('data1.csv','r') values = [] results = [] for line in f: fields = line.strip().split(',') if len(fields) == 4: x = float(fields[0]) y = float(fields[1]) z = float(fields[2]) values.append([math.sqrt(x**2 + y**2 + z**2)]) results.append(float(fields[3])) f.close() print('number of values: {}'.format(len(values))) Teraz musimy zmienić trochę postać danych. Plik wejściowy to 6467 wierszy z wynikami. Do uczenia sieci chcemy jednak wykorzystać okna po 128 wyników, które dają na wyjściu odpowiedź czy wykryto uczony wzorzec, czy nie. Co więcej dane powinniśmy podzielić na część treningową oraz testową - właściwie powinna być jeszcze trzecia część przeznaczona do walidacji, jednak jako prosty pierwszy przykład mam tylko dwie części: trening i test. train_values = [] train_labels = [] test_values = [] test_labels = [] split = len(values) * 0.8 for i in range(len(values) - 128): if i < split: train_values.append(values[i:i+128]) train_labels.append(results[i+128]) else: test_values.append(values[i:i+128]) test_labels.append(results[i+128]) train_values = np.array(train_values).reshape(-1, 128).astype('float32') train_labels = np.array(train_labels).astype('float32') test_values = np.array(test_values).reshape(-1, 128).astype('float32') test_labels = np.array(test_labels).astype('float32') Przyznać się muszę do jeszcze jednego niedociągnięcia - dane powinny być również znormalizowane. Najpierw planowałem wykorzystać wszystkie składowe XYZ, a wówczas normalizacja nie była konieczna. Jednak przeliczenie przyspieszenia na wartość wypadkową wprowadziło stałą odpowiadającą przyspieszeniu ziemskiemu - a w skryptach już tego niestety nie uwzględniłem. Teraz najważniejsza część czy uczenie samej sieci. Struktura jest banalnie prosta i pochodzi bezpośrednio z przykładów opisywanych w książce "Deep Learning. Praca z językiem Python i biblioteką Keras" train_labels = to_categorical(train_labels) test_labels = to_categorical(test_labels) network = models.Sequential() network.add(layers.Dense(16, activation='relu', input_shape=(128,))) network.add(layers.Dense(2, activation='softmax')) network.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy']) network.fit(train_values, train_labels, epochs=5, batch_size=8) network.summary() Po uruchomieniu kodu zobaczymy podsumowanie naszej sieci: Jak widzimy dokładność uczenia to 0.9086, moglibyśmy uzyskać dużo lepsze rezultaty tworząc bardziej wyszukaną sieć, ale przykład jest ogromnym uproszczeniem. Warto zwrócić uwagę na liczbę parametrów: 2064 dla pierwszej i 34 dla drugiej warstwy. Musimy pamiętać, że te parametry trzeba będzie zapisać w pamięci mikrokontrolera - jednak niewiele ponad 2000 parametrów to bardzo mało i pewnie dałoby się uruchomić nawet na STM32F0. Zanim zakończymy tworzenie sieci, jeszcze dwie ważne sprawy. Pierwsza to testowanie. Wynik, który widzieliśmy wcześniej dotyczy danych, na których sieć była uczona - jednak ważniejsze jest jak zachowa się z danymi, których "nie widziała". Podczas przygotowywania danych 20% zostało zapisane w zmiennych test_values, test_results i teraz mogą posłużyć do sprawdzenia jak sieć radzi sobie z nowymi informacjami: Uzyskujemy dokładność na poziomie 89.35%, czyli podobnie jak dla danych użytych do trenowania sieci. Ostatnia rzecz to zapisanie samej sieci do pliku, czyli wywołanie: network.save('data2.h5') Teraz mamy plik data2.h5, który jest gotowy do użycia z X-CUBE-AI, co opiszę w kolejnej części. data2.zip Spis treści serii artykułów: Sztuczna inteligencja na STM32, czyli przykład użycia X-CUBE-AI cz.1 Sztuczna inteligencja na STM32, czyli przykład użycia X-CUBE-AI cz.2 Sztuczna inteligencja na STM32, czyli przykład użycia X-CUBE-AI cz.3
  8. Algorytmy związane z szeroko pojętą sztuczną inteligencją (AI, Artificial Intelligence) są ostatnio bardzo popularnym tematem. Na ogół AI kojarzy się z ogromnym zapotrzebowaniem na moc obliczeniową, wykorzystaniem GPU (Graphics Processing Unit) oraz obliczeniami w chmurze. Okazuje się jednak, że sztuczna inteligencja znajduje również zastosowanie w świecie mikrokontrolerów. Powstał nawet termin na określenie takich zastosowań - Edge AI. O tym jak popularny jest temat AI świadczy chociażby pojawienie się sprzętowych akceleratorów przeznaczone dla urządzeń wbudowanych, tzw. TPU (Tensor Processing Unit), przykładowo Google Coral, albo Intel Neural Compute Stick. Dostępne są również mikrokontrolery z wbudowanym TPU, np. Kendryte K210 . Spis treści serii artykułów: Sztuczna inteligencja na STM32, czyli przykład użycia X-CUBE-AI cz.1 Sztuczna inteligencja na STM32, czyli przykład użycia X-CUBE-AI cz.2 Sztuczna inteligencja na STM32, czyli przykład użycia X-CUBE-AI cz.3 Firma STMicroelectronics, czyli producent popularnych również na naszym forum układów STM32 postanowiła dostarczyć produkt związany z AI. Na razie nie oferuje sprzętowej akceleracji, ale możemy za darmo pobrać pakiet oprogramowania o nazwie X-CUBE-AI, który pozwala na łatwe wykorzystanie możliwości jakie daje sztuczna inteligencja. W niniejszym artykule postaram się opisać bardzo prosty przykład wykorzystania biblioteki X-CUBE-AI na płytce ewaluacyjnej z układem STM32L475. Planowałem napisać dłuższy artykuł na zakończony niedawno konkurs, ale w trakcie przygotowywania projektu odkryłem jak dużo muszę się jeszcze sam nauczyć, zanim będę w stanie dzielić się wiedzą. Jednak to co udało mi się poznać i zrobić postaram się opisać w tym temacie. Proszę jednak o wyrozumiałość, tematem sztucznej inteligencji zajmuję się wyłącznie hobbistycznie i cały czas się uczę, więc z góry przepraszam za niedoskonałości opisywanego rozwiązania. Wybór platformy sprzętowej Sztuczna inteligencja nie bez powodu kojarzy się z ogromnym zapotrzebowaniem na pamięć i moc obliczeniową, rozpoznawanie mowy lub obrazu jest chyba dobrym przykładem, gdzie moc chmury wydaje się niezbędna. Okazuje się jednak, że AI można również zastosować w znacznie mniej wymagających obszarach. Jakiś czas temu na forum pojawił się opis projektu czujnika HRV: Wykrywanie bicia serca na podstawie sygnału jest jednym z przykładów, gdzie algorytmy sztucznej inteligencji mogą znaleźć zastosowanie. Ja co prawda nie jestem entuzjastą projektowania urządzeń medycznych metodami mocno hobbistycznymi, ale wspomniany artykuł zachęcił mnie do wypróbowania X-CUBE-AI do wykrywania wzroca w pewnym sygnale. Jako platformę testową wybrałem płytkę B-L475E-IOT01A, wyposażoną w mikrokontroler STM32L475 oraz całkiem sporo interesujących modułów peryferyjnych, a co najważniejsze w akcelerometr LSM6DSL. Płytka posiada ogromny potencjał, ja wykorzystam tylko mały jej fragment: port UART do komunikacji z PC, przede wszystkim do wysyłania danych akcelerometr posłuży jako źródło sygnału przycisk wykorzystam do uczenia sieci dioda LED będzie sygnalizowała wykrycie gestu Zbieranie danych treningowych Danymi źródłowymi będą pomiary z akcelerometru. Podczas uczenia, po wykonaniu gestu, który chcę nauczyć sieć będę naciskał przycisk na płytce. Dane treningowe mają więc postać 4 liczb: odczytu z akcelerometru dla osi XYZ oraz stanu przycisku. Ponieważ obraz jest wart tysiąca słów poniżej filmik z uczenia sieci: Moim celem było nauczenie sieci wykrywania dwukrotnego machnięcia płytką. W sumie nie wyszło mi to trenowanie do końca, bo wcale nie łatwo jest taki gest wielokrotnie wykonywać i się nie pomylić. Więc ostatecznie sieć ma wykrywać dwu- lub więcej- krotne pobujanie płytką. Jednokrotne, albo dwukrotne wykonane z odstępem gesty mają być ignorowane. Oczywiście można sieć nauczyć właściwie dowolnego gestu, pod warunkiem oczywiście, że sami jesteśmy w stanie ten gest wielokrotnie wykonać (i nie stracimy cierpliwości). Dane z akcelerometru odczytuję co 20ms. Z moich obliczeń wyszło, że 128 wyników, czyli 2.56s w zupełności wystarcza na wykonanie interesującego mnie gestu. Wyniki pomiarów wysyłane są przez port szeregowy Uczenie polegało na włączeniu zapisu danych do pliku oraz cierpliwym machaniu płytką oraz naciskaniu przycisku po wykonaniu uczonego gestu. W wyniku powstał plik data1.csv data1.zip Program do zbierania danych został napisany w środowisku STM32CubeIDE, dzięki temu jego kod będzie można później wykorzystać od testowania wytrenowanej sieci. Sam program jest właściwie banalny, do jego napisania wystarczy w zupełności materiał kursu STM32F4. Skoro mamy już dane treningowe, czas przystąpić do najważniejszego, czyli uczenia sieci. W następnej części postaram się opisać jak możemy wykorzystać zebrane dane. Spis treści serii artykułów: Sztuczna inteligencja na STM32, czyli przykład użycia X-CUBE-AI cz.1 Sztuczna inteligencja na STM32, czyli przykład użycia X-CUBE-AI cz.2 Sztuczna inteligencja na STM32, czyli przykład użycia X-CUBE-AI cz.3
  9. Bez podstaw pythona raczej nie da się używać keras, ani nic sensownego w AI zrobić. Nie musisz być od razu pythonowym guru, ale postawy są niezbędne. Inaczej zostanie tylko uruchamianie gotowców i zabawa przygotowanymi przez kogoś sieciami.
  10. Wydaje mi się, że jeśli ktoś chce poznać lepiej uczenie maszynowe, to do tego przyda się dobra książka i bardzo mocny komputer, najlepiej z solidną kartą graficzną. Natomiast użycie AI w systemach embedded to trochę inny temat. I tak trzeba sieć wytrenować na PC, później zostaje już tylko konwersja na mikrokontroler i w tym faktycznie może pomóc AI-CUBE. Miałem o tym napisać artykuł, ale trochę zabrakło czasu.
  11. Pytanie tylko czy @SOYER jest bardziej zainteresowany nauką budowy frezarek, czy raczej ich wykorzystania. Bo zbudowanie frezarki, czy drukarki to dopiero początek. Ale oczywiście jak ktoś ma mnóstwo wolnego czasu i dużo pieniędzy, to niech buduje od podstaw, w końcu kto bogatemu zabroni
  12. To ja podam argument żeby NIE budować czegoś takiego samemu. Pierwszą drukarkę 3d składałem sam - działała bardzo marnie, narobiłem się przy niej mnóstwo, ale właściwie zanim zdobyłem doświadczenie należałoby zacząć od początku. Drugą kupiłem gotową, składaną przez kogoś - było dużo lepiej, ale nadal daleko do ideału, a im dłużej jej używałem tym gorzej działała. Trzecią kupiłem gotową, tanią chińską - kosztowała praktycznie tyle co pierwsza, a jakbym doliczył części dokupione później do mojego składaka, to zdecydowanie mniej. Natomiast jakość wydruków gotowej drukarki była o wiele lepsza od obu pierwszych razem wziętych. Nie wiem jak to się ma do CNC, czy laserów, ale seryjnie produkowane urządzenie po prostu działa lepiej niż składane samemu - albo raczej działa lepiej niż kilka pierwszych składaków, przy których składający dopiero się uczy... Natomiast jeśli policzyłbym czas spędzony na takim składaniu, to taniej wyjdzie nie tylko gotowa chińska, ale i porządna, firmowa drukarka. Z laserami dochodzi jeszcze kwestia bezpieczeństwa - jak to ładnie kiedyś na laborkach było: na laser można popatrzeć dwa razy w życiu, raz lewym okiem, a raz prawym.
  13. Dyskusja nad wyższością jednego środowiska / procesora / biblioteki nad innym nie ma sensu. Żadne z tych rozwiązań nie jest absolutnie lepsze, ani absolutnie gorsze - gdyby tak było już dawno zostałoby tylko to lepsze. Natomiast nie mieszajmy edytora / środowiska z biblioteką. Arduino IDE jest... proste i chyba to wszystko co można napisać. Ale to dla wielu osób, szczególnie początkujących może być zaletą. Powiem więcej - nawet dla dawno temu początkujących może to być zaleta. Jak potrzebuję szybko przetestować np. nowy moduł, do którego mam bibliotekę pod Arduino to używam Arduino IDE. Bo tak jest po prostu najszybciej. Później mogę używać innych środowisk, bibliotek, czy mikrokontrolerów, ale żeby szybko przetestować np. nowy mały wyśwetilacz, albo czujnik użycie Arduino jest po prostu oszczędnością czasu. Na koniec jeszcze odnośnie wydajności bibliotek - wcale nie zawsze biblioteki HAL są szybsze od Arduino. Przyznam, że pisząc artykuł o wyświetlaczach przeżyłem ogromne rozczarowanie biblioteką HAL. Jeśli będziemy wysyłać dane w małych paczkach, np. po bajcie, biblioteki Arduino są wydajniejsze niż HAL... Okazuje się, że implementacja od ST jest tak przekombinowana, że zajmuje to mnóstwo czasu. Natomiast Arduino zapisuje od razu do rejestrów. Ot taka ciekawostka.
  14. Używanie bibliotek Arduino na STM32, jak i dowolnym innym mikrokontrolerze ma jak najbardziej sens. Wbrew nazwie użytej przez ST, ich HAL nie zapewniaja właściwie żadnej abstrakcji sprzętu, a teoretycznie powinien bo w końcu HAL to skrót od Hardware Abstraction Layer. Przykra prawda jest niestety taka, że używając STM32 HAL trzeba nadal dobrze znać sprzęt, nie możemy zmienić mikrokontrolera na inny bez modyfikacji wyższych warstw programu, a nawet biblioteki HAL dla różnych rodzin STM32 nie są ze sobą w pełni zgodne. Natomiast Arduino - niezależnie od oceny jakości kodu, zapewnia prawdziwą abstrakcję sprzętu, czyli prawdziwy HAL. Dzięki tym bibliotekom można ten sam program np. blink uruchomić zarówno na stm32, atmedze, czy esp8266. Jak napisałem wcześniej, nie jest to szczyt osiągnięć sztuki programistycznej, ale zawsze jakiś standard. Więc jak chodzi o sens to po pierwsze jest łatwiej, po drugie mamy niezależność od sprzętu. Warto też pamiętać, że kod biblioteki STM32 HAL jest również bardzo daleki od optymalnego, natomiast to co generuje CubeMX nie dość że jest paskudne, to jeszcze nie zawsze działa.
  15. O stm32mp1 są na Forbocie co najmniej dwa artykuły: https://forbot.pl/blog/nowe-stm32-ze-wsparciem-dla-linuksa-cos-dla-fanow-rpi-id33971 https://forbot.pl/blog/stm32mp1-co-warto-wiedziec-jak-zaczac-id37003 Ogólnie ten mikroprocesor jest bardziej interesujący, ale raczej ma niewiele wspólnego z instalacją Linux-a na mikrokontrolerach, co było opisane w tym temacie. Co do komunikacji między rdzeniami, to używana jest biblioteka OpenAMP (https://github.com/OpenAMP/open-amp/wiki/OpenAMP-Overview), ale najprościej jest użyć wirtualnego uart-a. W przykładach od ST znajdziemy gotowe rozwiązanie, które działa tak jakby oba rdzenie były połączone łączem szeregowym (po stronie linuxa używamy zwykłego urządzenia /dev/tty*, po stronie mikrokontrolera mamy gotowe funkcje do wysyłania i odbierania danych). Natomiast przypisanie GPIO i większości modułów peryferyjnych można samemu zmodyfikować, więc jeśli chcemy to rdzeń cortex-m może kontrolować więcej GPIO niż cortex-a. Wszystko zależy od zastosowania. Niektóre moduły oraz ich piny (np. pamięć DDR) są dostępne tylko dla cortex-a, ale na pewno nie jest to większość.
  16. Mikrokontrolery nie mają MMU. Dostępny jest mikroprocesor stm32mp1 z mmu, ale artykuł dotyczył mikrokontrolerów.
  17. @marek1707 nie jestem adminem, ani moderatorem, nie mam więc możliwości zamknięcia tego tematu. Inna sprawa, że przeniesienie go do działu „na luzie” byłoby wskazane, ale już samo zamykanie - właściwie dlaczego? Nikt nic złego nikomu w tym temacie nie zrobił, mamy dość ciekawe hasło, chociaż jego rozwinięcie raczej nie ma żadnego sensu. Moim zdaniem najlepsze co wszyscy razem możemy zrobić, to przestać się w tym temacie odzywać i już nie karmić trola.
  18. Żeby się cieszyć, najpierw trzeba uwierzyć że opisywane rozwiązanie istnieje i jest cokolwiek warte. Na razie słyszymy tylko przechwałki i nic konkretnego. Więc jak dla mnie więcej treści miał offtop o teatrze. I jak na razie bajki dla dzieci są dla mnie bardziej wiarygodne niż ten tajemniczy wynalazek - ale bardzo chętnie zmienię zdanie, tylko daj nam szansę i napisz cokolwiek, bo pisać że wiem ale nie powiem to każdy umie... I chyba lepiej brzmi: za siedmioma górami, za siedmioma lasami..
  19. 14 wpisów, mnóstwo tekstu i zupełnie brak treści... Proponuję przejść do jakiegokolwiek konkretu, albo wywalić ten wątek do kosza. Forbot to techniczne forum, a nie kółko filozoficzne, @MKJB ja coś wymyśliłeś, może zbudowałeś i chcesz o tym napisać, to pisz śmiało, chętnie poczytamy
  20. Nie bardzo wiem co miałoby dać @SOYER -owi użycie DMA. Był na forum niedawno użytkownik, który co chwila powtarzał że bez DMA nie ma życia... ale dla kogoś kto jeszcze nie opanował wszystkich niuansów arduino to nie musi być takie ważne. Z drugiej strony są po prostu nowsze moduły arduino, chociażby z rodziny MKR, albo wspomniane ESP.
  21. Używanie Cube bywa czasem zaskakujące. Przykładowo generator kodu, czyli CubeMX potrafi utworzyć zapełenie niedziałający kod, a doszukanie się przyczyny błędu zajmuje więcej niż jego napisanie od podstaw. Z samą biblioteką HAL też bywa niewesoło - mało kto chyba zdaje sobie sprawę, że np. w kodzie obsługi przerwań HAL posiada pętle z aktywnym czekaniem... łatwo można napisać piękny program, który będzie działał tylko czasami. Co więcej w dokumentacji biblioteki nic o takich "kwiatkach" nie znajdziemy. A przeglądanie kodu to nic przyjemnego, bo napisany jest tak, że czasem lepiej czytać z zamkniętymi oczami. Więc tak jak @marek1707 napisał - STM32 są bardzo fajne, biblioteka Cube HAL też interesująca, ale to na pewno nie jest łatwe, miłe i przyjemne dla początkujących.
  22. A może warto zacząć od zapytania wujka google? Wpisujesz np. "AMD FX 6th gen 8800P error 43" i masz pierwszy wynik: https://windowsreport.com/amd-error-43-windows-10/ Wygląda więc na to, że nie jesteś jedynym posiadaczem błędu 43, a problem wynika ze złych sterowników. Pewnie jak poszukasz dokładniej w google znajdziesz pełne i działające rozwiazanie problemu.
  23. W poprzednich odcinkach zobaczyliśmy jak działa program, który przesyła do wyświetlacza dane dla każdego piksela osobno, następnie przetestowaliśmy wersję z buforem dla całej pamięci obrazu. Pierwsza wersja działała bardzo wolno, ale zużywała mało pamięci RAM. Druga działała bardzo szybko, ale bufor zajmował ogromną jak na mikrokontroler ilość pamięci. Teraz spróbujemy przygotować wersję pośrednią - tym razem użyjemy mniej pamięci, ale więcej czasu procesora. Spis treści: Sterowanie wyświetlaczem TFT - część 1 - wstęp, podstawowe informacje Sterowanie wyświetlaczem TFT - część 2 - analiza problemu Sterowanie wyświetlaczem TFT - część 3 - testy prędkości na STM32 Sterowanie wyświetlaczem TFT - część 4 - własny program Sterowanie wyświetlaczem TFT - część 5 - optymalizacja programu Obliczanie danych obrazu Pełny bufor obrazu jak pamiętamy zajmuje 160 x 128 x 2 = 40960 bajtów pamięci. Takie rozwiązanie zapewniło nam możliwość szybkiego tworzenia grafiki w pełnej rozdzielczości oraz 65 tysiącach kolorów. Jednak w wielu zastosowaniach wystarczyłaby nieco mniejsze możliwości, przykładowo gdybyśmy zamiast 16-bitów zastosowali 8, uzyskalibyśmy 256 kolorów, a jednocześnie zmniejszyli zużycie pamięci o połowę. W wielu przypadkach nawet 16 kolorów, czyli 4 bity mogłyby wystarczyć, a jak łatwo policzyć bufor zajmowałby wówczas 10240 bajtów. Podobnie z rozdzielczością, jeśli tworzymy np. mini konsolę do grania, tryb 80x64 mógłby nam wystarczyć, nadałby nawet nieco stylu "retro". Ogólna idea jest więc taka, że spróbujemy przechowywać mniejszą ilość danych, a następnie przeliczać je na reprezentację oczekiwaną przez wyświetlacz dopiero przed wysłaniem. Tryb z paletą kolorów Metod generowania obrazu jest mnóstwo, ja spróbuję przedstawić bardzo prosty, czyli użycie 8-bitowej palety. Bufor obrazu będzie wyglądał podobnie jak wcześniej, ale zamiast typu uint16_t użyjemy uint8_t: uint8_t lcd_framebuffer[LCD_WIDTH * LCD_HEIGHT]; Jak łatwo policzyć zajmuje on teraz 160 x 128 = 20480 bajtów, czyli nadal sporo, ale zawsze można zastosować kolejne optymalizacje. Wyświetlacz oczekuje danych w postaci RGB565, czyli 16-bitowych wartości gdzie 5-bitów określa składową czerwoną, 6-zieloną, a 5-niebieską. Paleta to po prostu 256-elementowa tablica, która zawiera wartości opisujące dany kolor: uint16_t palette[256]; W docelowym programie moglibyśmy dobrać idealną paletę do naszego zastosowania i zapisać ją w pamięci flash (albo w samym programie). Jak wspomniałem tutaj prezentuję jedynie demo, więc paletę będę obliczać: Prawdę mówiąc takie rozwiązanie nie wyglądało najładniej, bo nie można w nim reprezentować bieli, więc zamiast uzupełniać zerami, uzupełniłem jedynkami - jak napisałem, to tylko demo. Bardzo prosty kod przeliczający 8-bitową paletę, na 16-bitowy kolor dla wyświetlacza wygląda następująco: static inline uint16_t palette2rgb(uint8_t color) { uint16_t r = ((uint16_t)color & 0xe0) << 8; uint16_t g = ((uint16_t)color & 0x1c) << 6; uint16_t b = ((uint16_t)color & 0x03) << 3; return __REV16(0x18e7 | r | g | b); } Ta magiczna wartość 0x18e7 to właśnie uzupełnienie jedynkami - wiem że to brzydki kod, z góry za niego przepraszam, ale to mało istotny fragment, zachęcam oczywiście do zastosowania o wiele lepszych rozwiązań. Teraz przechodzimy do najważniejszego, czyli przeliczania 8-bitowych danych w buforze, na docelowe 16-bitowe przeznaczone dla wyświetlacza. Przesyłanie po jednym bajcie nie działa wydajnie, więc utworzymy nieduży bufor tymczasowy, ja ustaliłem jego wielkość na 512 pikseli: #define TX_BUF_SIZE 512 static uint16_t tx_buf[TX_BUF_SIZE]; Skoro wyświetlacz ma rozdzielczość 160 x 128, więc jak łatwo policzyć będziemy ten bufor wypełniać i wysyłać 40 razy, aby przesłać cały obraz. Funkcja do wypełniania bufora wygląda następująco: static void fill_tx_buf(uint32_t part) { uint16_t *dest = tx_buf; uint8_t *src = lcd_framebuffer + part * TX_BUF_SIZE; for (uint32_t i = 0; i < TX_BUF_SIZE; i++) *dest++ = palette2rgb(*src++); } Jako parametr podajemy indeks bufora, czyli wartość 0..39, po jej wykonaniu bufor tx_buf będzie zawierał dane do przesłania. Teraz możemy napisać funkcję rysującą zawartość naszego ekranu: void lcd_copy(void) { lcd_cmd(ST7735S_RAMWR); HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_RESET); for (uint32_t i = 0; i < LCD_WIDTH * LCD_HEIGHT / TX_BUF_SIZE; i++) { fill_tx_buf(i); HAL_SPI_Transmit(&hspi2, (uint8_t*)tx_buf, 2 * TX_BUF_SIZE, HAL_MAX_DELAY); } HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_SET); } Musimy jeszcze zmodyfikować naszą bibliotekę graficzną, tak żeby pracowała z 8-bitowymi kolorami, ale to właściwie kosmetyczna zmiana. Czas skompilować program: Zgodnie z oczekiwaniami zużycie pamięci RAM znacznie spadło i wynosi niecałe 23KB. Przetestujmy wydajność naszego programu: Jak widzimy rysowanie w lokalnym buforze zajmuje tyle samo czasu, czyli 7ms, natomiast kopiowanie trochę więcej niż poprzednio, bo 37ms zamiast 33ms. Warto przy okazji przetestować wydajność funkcji wypełniającej bufor, czyli fill_tx_buf: void lcd_test(void) { uint32_t start = HAL_GetTick(); for (uint32_t i = 0; i < 1000; i++) fill_tx_buf(i % (LCD_WIDTH * LCD_HEIGHT / TX_BUF_SIZE)); uint32_t end = HAL_GetTick(); printf("lcd_test: %ld us\r\n", end - start); } Wywołanie 1000 razy fill_tx_buf zajmuje 110ms, czyli jedno jej wywołanie ok. 110us. Jak pamiętamy używamy jej 40 razy, co zgadza się z pozostałymi pomiarami - trochę ponad 4ms zużyliśmy na obliczenia, ale zaoszczędziliśmy prawie 20KB pamięci. Użycie tablicy zamiast obliczeń pewnie pozwoliłoby skrócić ten czas, ale jak wspominałem, to tylko przykład. Nie będę wstawiał kolejnego filmu, bo już tyle razy widzieliśmy ekran testowy, że chyba każdy ma go dosyć. Czas udoskonalić nasz program. Użycie DMA W poprzedniej części korzystaliśmy z DMA, więc nowy program jest pod wieloma względami "gorszy". Użyjmy więc HAL_SPI_Transmit_DMA zamiast, HAL_SPI_Transmit. Procedura wysyłania będzie wyglądała następująco: void lcd_copy(void) { lcd_wait_ready(); lcd_busy = true; lcd_cmd(ST7735S_RAMWR); HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_RESET); tx_part = 0; fill_tx_buf(tx_part++); HAL_SPI_Transmit_DMA(&hspi2, (uint8_t*)tx_buf, 2 * TX_BUF_SIZE); } Wypełniamy w niej bufor pierwszym fragmentem obrazu i rozpoczynamy wysyłanie. W zmiennej tx_part przechowujemy informację o numerze kolejnego fragmentu do wysłania. Musimy teraz obsłużyć przerwanie informujące o zakończeniu transmisji i w nim przygotować dane dla następnego fragmentu, albo zakończyć całą operację: void lcd_copy_done(void) { if (tx_part < LCD_WIDTH * LCD_HEIGHT / TX_BUF_SIZE) { fill_tx_buf(tx_part++); HAL_SPI_Transmit_DMA(&hspi2, (uint8_t*)tx_buf, 2 * TX_BUF_SIZE); } else { HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_SET); lcd_busy = false; } } Po uruchomieniu zobaczymy, że program działa tak samo jak poprzednio. Różnica jest jednak taka, że podczas transmisji przez DMA procesor może wykonywać inne zadania. Jednak czas kopiowania obrazu nadal wynosi 37ms. Jeszcze jedna ważna uwaga - w przerwaniu generujemy dane dla kolejnego bufora, więc na 110us blokujemy przerwanie. Długie procedury obsługi przerwań to nic dobrego, ale STM32 pozwala na szczęście na ustawienie priorytetów przerwań. Możemy więc przerwaniu od DMA nadać niski priorytet i dzięki temu nasze obliczenia nie będą opóźniały innych, pilniejszych zadań: Użycie podwójnego bufora Jeśli podłączymy analizator logiczny to zobaczymy, że komunikacja z wyświetlaczem ma "przerwy". Wynika to stąd, że gdy obliczamy dane dla kolejnego fragmentu ekranu komunikacja jest zatrzymywana. Możemy trochę skomplikować nasz program, ale jednocześnie przyspieszyć działanie. Tym razem użyjemy dwóch buforów, albo raczej jednego większego. Gdy DMA będzie wysyłało jedną część danych, będziemy mieli czas na przygotowanie następnej. Deklarujemy więc większy bufor: #define TX_BUF_SIZE 512 static uint16_t tx_buf[TX_BUF_SIZE * 2]; Funkcja wypełniania bufora będzie teraz zapisywać parzyste fragmenty w pierwszej części tx_buf, a nie nieparzyste w drugiej: static void fill_tx_buf(uint32_t part) { uint16_t *dest = (part % 2 == 0) ? tx_buf : tx_buf + TX_BUF_SIZE; uint8_t *src = lcd_framebuffer + part * TX_BUF_SIZE; for (uint32_t i = 0; i < TX_BUF_SIZE; i++) *dest++ = palette2rgb(*src++); } Przed rozpoczęciem transmisji wypełnimy nie jeden, ale dwa bufory: void lcd_copy(void) { lcd_wait_ready(); lcd_busy = true; lcd_cmd(ST7735S_RAMWR); HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_RESET); tx_part = 0; fill_tx_buf(tx_part++); fill_tx_buf(tx_part++); HAL_SPI_Transmit_DMA(&hspi2, (uint8_t*)tx_buf, 4 * TX_BUF_SIZE); } Ostatnia zmiana to obsługa nowego przerwania, które będzie wywoływane po przesłaniu połowy danych: void HAL_SPI_TxHalfCpltCallback(SPI_HandleTypeDef *hspi) { lcd_copy_halfdone(); } Gdy otrzymamy to przerwanie, będziemy po prostu wypełniać następny bufor: void lcd_copy_halfdone(void) { fill_tx_buf(tx_part++); } Natomiast procedura obsługi końca transmisji prawie się nie zmieniła, jedyna różnica to wielkość bufora: void lcd_copy_done(void) { if (tx_part < LCD_WIDTH * LCD_HEIGHT / TX_BUF_SIZE) { HAL_SPI_Transmit_DMA(&hspi2, (uint8_t*)tx_buf, 4 * TX_BUF_SIZE); fill_tx_buf(tx_part++); } else { HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_SET); lcd_busy = false; } } Program jest nieco bardziej skomplikowany, ale w nagrodę otrzymaliśmy czas kopiowania identyczny jak w wersji z poprzedniego odcinka, ale zużyliśmy mniej pamięci: Na zakończenie jeszcze mały przykład wykorzystania naszego nowego programu. Program demonstracyjny Jakiś czas temu na forum pojawił się wątek z pytaniem o sposób wyświetlania kołowego progres baru. Pytanie sprowokowało ciekawą dyskusję na temat możliwości wydajnej realizacji takiego zadania. Skoro mamy opanowane sterowanie wyświetlacza TFT, możemy spróbować narysować nieco podobny element, a przy okazji sprawdzić jak nasza "biblioteka" sprawdzi się w realnym przykładzie. Zacznijmy od małej powtórki z matematyki oraz pewnego uproszczenia. Dla ułatwienia rysujmy tylko połowę progres-bara, zawsze możemy później rozbudować program. Rysowanie pionowych linii jest na ogół dość szybką operacją, więc zastanówmy się jak narysować wykres pionowymi (albo poziomymi) liniami. Kąt alfa oraz promienie r1 i r2 to nasze dane. Ja wybrałem rysowanie pionowych linii, więc x będzie zmienną. Wszyscy pamiętamy z matematyki wzór okręgu: x2 + y2 = r2. Po karkołomnych przejściach matematycznych uzyskujemy więc: y1 = sqrt(r1 - x2) y3 = sqrt(r2 - x2) Funkcja sqrt to pierwiastek. Jeśli ktoś jest miłośnikiem optymalizacji to wartości y1 i y2 może raz policzyć i trzymać w tablicy (najlepiej w pamięci Flash). Jak wspominałem program to demo, więc na razie nie będzie aż tak optymalny. Zostaje jeszcze obliczenie y2. Jest to odrobinę trudniejsze, może wymagać szybkiej powtórki z trygonometrii, a jak pamiętamy tg(alpha) = y/x, więc: y2 = x * tg(alpha) Tutaj znowu możemy tablicować wartości, ale na początek zostawmy prostą wersję. Mamy więc dla każdego x obliczone y1, y2 i y3. Teraz wystarczy sprawdzić jak y2 ma się do pozostałych i są możliwe 3 przypadki: jeśli y2 <= y1 to nic nie rysujemy jeśli y2 >= y3 to rysujemy "pełną" linię od y1 do y3 a jeśli y1 < y2 < y3 to linię od y1 do y2 Rysowanie możemy więc wykonać następującym programem: void draw_bar(uint32_t r1, uint32_t r2, uint32_t alpha) { float t = tan(alpha * M_PI / 180.0); bar_circle(0, 0, r1, WHITE); bar_circle(0, 0, r2, WHITE); bar_line(0, 0, r1 * cos(alpha * M_PI / 180.0) * 0.8f, r1 * sin(alpha * M_PI / 180.0) * 0.8f, WHITE); for (uint32_t x = 0; x <= r2; x++) { uint32_t y1 = (x < r1) ? sqrt(r1 * r1 - x * x) : 0; uint32_t y2 = sqrt(r2 * r2 - x * x); uint32_t y3 = x * t; if (y3 > y2) bar_line(x, y1, x, y2, WHITE); else if (y3 > y1) bar_line(x, y1, x, y3, WHITE); } } Funkcje bar_circle i bar_line są dodane aby "przenieść" nasz początek współrzędnych w odpowiednie miejsce: static void bar_circle(uint32_t x, uint32_t y, uint16_t r, uint8_t color) { lcd_circle(LCD_WIDTH - 1 - x, LCD_HEIGHT - 1 - y, r, color); } static void bar_line(uint32_t x1, uint32_t y1, uint32_t x2, uint32_t y2, uint8_t color) { lcd_line(LCD_WIDTH - 1 - x1, LCD_HEIGHT - 1 - y1, LCD_WIDTH - 1 - x2, LCD_HEIGHT - 1 - y2, color); } Dodajmy jeszcze "wskazówkę", pomiar czasu działania oraz wyświetlanie wartości: void draw_test_screen(uint32_t value) { char buf[32]; uint32_t start = HAL_GetTick(); lcd_clear(BLUE); draw_bar(117, 127, value); sprintf(buf, "%ld", value); lcd_fill_rect(110, 90, 150, 120, BLACK); lcd_draw_string(120, 100, buf, &Font16, WHITE, BLACK); lcd_copy(); uint32_t time = HAL_GetTick() - start; printf("drawing: %lu ms\r\n", time); } Teraz program jest gotowy: Nawet bez tablicowania wartości i z arytmetyką zmiennopozycyjną rysowanie zajmuje ok 22ms. Kopiowanie to 33ms, mamy więc prawie 20 klatek na sekundę, ale pewnie dałoby się więcej. Podsumowanie Początkowo planowałem napisanie jednego, może dwóch artykułów odnośnie sterowania wyświetlaczem TFT. Okazało się jednak, że temat jest o wiele obszerniejszy i ciekawszy, a 5 części to właściwie dopiero wstęp. Mam nadzieję, że udało mi się pokazać jak można sterować wyświetlaczem kolorowym oraz zachęcić do własnych eksperymentów i udoskonalania zaprezentowanych rozwiązań. Spis treści: Sterowanie wyświetlaczem TFT - część 1 - wstęp, podstawowe informacje Sterowanie wyświetlaczem TFT - część 2 - analiza problemu Sterowanie wyświetlaczem TFT - część 3 - testy prędkości na STM32 Sterowanie wyświetlaczem TFT - część 4 - własny program Sterowanie wyświetlaczem TFT - część 5 - optymalizacja programu Ten wpis bierze udział w konkursie na najlepszy artykuł o elektronice lub programowaniu, którego partnerem jest firma PCBWay (popularny producent prototypów PCB). W puli nagród karty podarunkowe Allegro o wartości 2300 zł. Sprawdź więcej informacji na temat konkursu »
×
×
  • Utwórz nowe...