Popularny post Gieneq Napisano Sierpień 29, 2021 Popularny post Udostępnij Napisano Sierpień 29, 2021 (edytowany) Choć tablice nie są tematem kursu Arduino ani poziomu 1 ani poziomu 2, to warto o nich wspomnieć. W pytaniach na forum kursanci często pytają jak udoskonalić swoje programy – w niektórych przykładach aż prosi się o użycie tablic. Temat jest omówiony co prawda w kursie STM32L4 w części 3 o GPIO, to jednak linkowanie trudnego artykułu dla zupełnie początkujących może budzić lęk. Postanowiłem więc napisać wstęp do tablic w Arduino, który przerodził się w nieco dłuższą serię (powiedzmy kurs). W pierwszej części ogólnikowo przedstawię tematykę oraz sposób poradzenia sobie z kilkoma typowymi problemami. W dalszych częściach zagłębimy się w bardziej wnikliwe zagadnienia, gdzie niezbędna będzie podstawowa wiedza o wskaźnikach. Spis treści #1 – wstęp i praktyka (czytasz ten artykuł) #2 – organizacja pamięci #3 – tablice w pamięci #4 – tablica jest wskaźnikiem #5 – znaki, cstring #6 – argumenty funkcji #7 – przykład #8 – tablice wielowymiarowe #9 – tablice dynamiczne #10 – zakończenie Zmienne i typy danych W kursie Arduino przyrównano zmienne do "szufladek" do których wkładamy jakieś dane. Przeznaczenie tych danych określa typ danych i na pewno kojarzysz już takie hasła jak: char, int, long czy float. Każda zmienna jest w stanie przechować jedną wartość, np. zmienna int może przechować liczbę całkowitą, a zmienna float liczbę zmiennoprzecinkową: int variable_int = 5; // zmienna całkowitoliczbowa float variable_float = 1.5; // zmienna zmiennoprzecinkowa Obiekt jest tu słusznym określeniem, ponieważ w C i C++ istnieją typy danych wbudowane (tzw. fundamentalne lub proste), np. char, int, ipt., a także typy dodane, które mogą reprezentować zbiór (np. strukturę) typów fundamentalnych. Więcej danych Co jednak zrobić, gdy liczba danych zaczyna rosnąć? Wyobraźmy sobie taki scenariusz: projektujemy sterownik skrytek na bagaż. W każdej skrytce ma być umieszczony czujnik zamknięcia drzwiczek, tak by sterownik miał kontrolę nad całą szafą: Gdybyśmy chcieli przyporządkować każdemu czujnikowi (skrytce) jedną zmienną to szybko dojdziemy do 2 słusznych wniosków: napisanie tego jest mozolne i małorozwojowe, trzeba jakoś usystematyzować nazewnictwo. Możliwe, że nasz kod zacznie wyglądać tak: // skrytki w górnym wierszu int locker_0 = 0; // skrytka w zerowej kolumnie int locker_1 = 0; // skrytka w pierwszej kolumnie int locker_2 = 0; // skrytka w drugiej kolumnie int locker_3 = 0; // skrytka w trzeciej kolumnie int locker_4 = 0; // skrytka w czwartej kolumnie // ... Jak się teraz do tego odwołać? Jak sprawdzić czy wybrana skrytka jest zamknięta? Lepiej nie zaprzątać sobie tym głowy. Użyjmy do tego tablicy! Tablice Tablica jest uporządkowanym ciągiem danych tego samego typu zajmujących ciągłe miejsce w pamięci. Wracając do przykładu podanego na wstępie, to tablica jest odpowiednikiem szafki z szufladami. Szuflady możemy ponumerować począwszy od zera, a następnie wkładać i wyciągać z nich obiekty posługując się tymi numerami, czyli indeksami. Dane są uporządkowane - każda skrytka ma swój niezmienny, unikatowy indeks. To że są tego samego typu oznacza, że w obrębie jednej tablicy możemy używać tylko jednego typu danych – to tak jakby uznać, że jedna szafka jest tylko na sztućce, a inna tylko na narzędzia. Do tego, że dane zajmują w pamięci określone miejsce jeszcze wrócimy. Tablice w praktyce Postaram się w skrócie napisać niezbędne minimum, aby móc używać tablic w programach (szczegóły zostaną omówione w dalszych częściach). Zadeklarujmy przykładową tablicę składającą się z 5 elementów: int lockers[5]; Deklaracja ta oznacza zarezerwowanie miejsca na 5 zmiennych typu int. Jak widać różnica polega tylko na dodaniu nawiasu kwadratowego po prawej stronie nazwy zmiennej z liczbą 5 oznaczającą liczbę komórek tablicy. Uwaga! Liczba komórek w takiej tablicy (statycznej) jest liczbą stałą i koniecznie musi być znana na etapie kompilacji, czyli w jakiś sposób jawnie zapisana w programie. Istnieje możliwość utworzenia tablicy (dynamicznej) w czasie działania programu. Jeżeli taką deklarację umieścisz "na górze kodu" poza jakąkolwiek funkcją (w tym setup), to domyślnie każdy element tablicy będzie miał wartość 0. Możliwe jest jednak zainicjowanie tablicy własnymi wartościami – należy je podać w nawiasach klamrowych zgodnie z przykładem: int lockers[5] = {1, 0, 1, 1, 1}; Odczyt wartości tablicy Aby upewnić się czy udało się zapisać w ten sposób dane do tablicy, odczytajmy kilka jej elementów: Serial.println(lockers[0]); Serial.println(lockers[1]); Serial.println(lockers[2]); Należy pamiętać, że indeksowanie w tablicach zaczynamy od 0. W wyniku powinniśmy otrzymać: 1 0 1 Jak można zauważyć, wpisywanie kolejnych indeksów jest niepraktyczne i można się przy tym pomylić, użyjmy więc pętli for do wypisania wszystkich elementów: for(int i = 0; i < 5; ++i) Serial.println(lockers[i]); Należy pamiętać, że ostatni indeks 5 elementowej tablicy to 4, a nie 5. Zapis elementów tablicy Podobnie jak w zwykłych zmiennych, w tablicach też można coś zapisać. Wystarczy przypisać wartość do konkretnego elementu: lockers[0] = digitalRead(3); Podobnie jak przy odczytywaniu wartości, również do zapisu można wykorzystać pętlę: for(int i = 0; i < 5; ++i) lockers[i] = 1; Do czego praktycznego można w Arduino użyć tablice? Sprawdźmy to na przykładzie odczytu wejść Arduino. Praktyczny przykład Załóżmy, że do pierwszych 8 pinów Arduino mamy podpięte przyciski. Przyciski te mogą być przełącznikami krańcowymi umieszczonymi w robocie służące do wykrywania kolizji. Chcemy mieć możliwość zapisania odczytów z czujników do osobnych zmiennych. Jak wiemy napisanie kilku prawie identycznych zmiennych mija się z celem, lepiej użyć tablicę: #define SENSORS_COUNT 8 int sensors[SENSORS_COUNT]; void read_all_sensors() { for(int i = 0; i < SENSORS_COUNT; ++i) sensors[i] = digitalRead(i); } Taki zapis wygląda dużo lepiej. Zwracam od razu uwagę na staranne pisanie kodu: dbanie o wcięcia i nawiasy, tworzenie sensownych nazw i unikanie magicznych liczb zastępując je stałymi lub "przezwiskami" (aliasami dyrektywy #define). Indeksy tablicy można potraktować jako kolejne czujniki na obwodzie robota. W razie zmian w sprzęcie (dołożeniu/usunięciu czujników) łatwo można zmienić program, tak aby pasował do nowego układu. Co jednak, gdyby nie dało się tak dobrać wyprowadzeń Arduino, tak by piny zgadzały się z indeksami tablicy? Przyda się tu koncepcja tablicowania stałych. LUT – Lookup Table Tablicowanie zwane po angielsku Lookup Table pozwana na zastąpienie funkcji tablicą. Argumenty funkcji zamieniane są w indeksy tablicy, zaś wyniki funkcji są w wartościach komórek przyporządkowanych do indeksów. Wygląda to podobnie do opisu funkcji nauczanego w szkole: Po lewej stronie są indeksy tablicy, po prawej wartości elementów kryjących się pod tymi indeksami. Jak to może wyglądać w praktyce? Wróćmy do problemu postawionego w poprzednim przykładzie. Załóżmy, że czujniki w robocie są trochę chaotycznie połączone. Zanotowaliśmy połączenia, ale co z tym zrobić? nr czujnika, pin Arduino 0 , 4 1 , 5 2 , 7 3 , 8 4 , 3 5 , 6 6 , 11 7 , 10 Zainicjujmy tablicę przechowującą numery wyprowadzeń Arduino, do których podpięte są czujniki zgodnie z powyższymi notatkami: int sensors_pins[SENSORS_COUNT] = {4, 5, 7, 8, 6, 3, 11, 10}; Czy liczba w nawiasach kwadratowych jest potrzebna? Nie, gdyż kompilator sam by się domyślił jaki jest rozmiar tablicy na podstawie liczby wartości ujętych w klamrach. Jawne wpisanie liczby elementów pomaga uniknąć problemów, bo kompilator sam może wykryć nieścisłość. Jak się później dowiesz może to być też wykorzystane do innych celów. Warto przypomnieć: liczba elementów tablicy statycznej (czyli takiej której rozmiar się nie zmienia) musi być w jakiś sposób znana na etapie kompilacji. Tak przygotowana tablica umożliwia nabudowanie pewnego poziomu abstrakcji, tak by nie posługiwać się fizycznymi wyprowadzeniami, a zacząć posługiwać się indeksami. Teraz aby odczytać dane z czujników wystarczy nieco poprawić poprzednią funkcję: #define SENSORS_COUNT 8 int sensors[SENSORS_COUNT]; int sensors_pins[SENSORS_COUNT] = {4, 5, 7, 8, 6, 3, 11, 10}; void read_all_sensors() { for(int i = 0; i < SENSORS_COUNT; ++i) sensors[i] = digitalRead(sensors_pins[i]); } I gotowe! W zmiennej sensors są odczyty czujników ułożonych zgodnie z fizycznym rozkładem w robocie (a nie narzucone niekoniecznie oczywistymi połączeniami). Zastosowanie tablicowania pomogło naprawić niedopatrzenie i sprawiło, że program jest bardziej elastyczny i szybszy. Zdarza się, że nie możemy użyć pewnych wyprowadzeń Arduino, bo niektóre pełnią specyficzną rolę (np. realizują komunikację UART) – wtedy tablicowanie może okazać się niezbędne. Kapitanie... do brzegu – zadania z kursu Pisałem na wstępie o zadaniach z kursu i obiecuję, że zaraz jedno zrobimy, na początek coś na rozgrzewkę – napiszmy funkcję, która włączy wszystkie diody w linijce LED. Później dojdziemy do tego jak poradzić sobie z dość trudnym przykładem z czujnikiem ultradźwiękowym. Załóżmy, że wyprowadzenia zapisaliśmy w postaci etykiet o nazwach LED_0 – LED_5. Gdyby nie używać tablic to kod wyglądałby jakoś tak: void turn_on_leds() { digitalWrite(LED_0, HIGH); digitalWrite(LED_1, HIGH); digitalWrite(LED_2, HIGH); digitalWrite(LED_3, HIGH); digitalWrite(LED_4, HIGH); digitalWrite(LED_5, HIGH); } Nie jest źle, ale da się lepiej! Używając pętli i tablicy będzie to wyglądać tak: int LED_pins[] = {LED_0, LED_1, LED_2, LED_3, LED_4, LED_5}; void turn_on_leds() { for(int i = 0; i < LEDS_COUNT; i++) digitalWrite(LED_pins[i], HIGH); } Brak mozolnego kopiowania na pewno uprzyjemni pisanie takich kodów Zadanie 2 – prosta animacja Teraz coś nieco trudniejszego – program ma wyświetlać animację, w której zapalane i gaszone są kolejne diody. Tu już bardzo ważne jest poprawne indeksowanie. Bez wykorzystania tablic kod byłby bardzo powtarzalny i nieprzyjemny w modyfikacji. Stosując tablicę i pętlę zajmie to tylko kilka linijek kodu! for(int i = 0; i < LEDS_COUNT; i++) { digitalWrite(LED_pins[i], HIGH); delay(250); digitalWrite(LED_pins[i], LOW); delay(250); } I tyle, kolejne diody są zapalane i gaszone. Kod ten można umieścić wewnątrz pętli loop() aby wykonywał się w nieskończoność zapętlając animację. Zadanie 3 – wyświetlacz słupkowy Wreszcie obiecane zadanie Na tapetę weźmy zadanie 9.3 z kursu Arduno 1 i części dotyczącej ultradźwiękowego czujnika przeszkód, którego teść brzmi: Cytat Napisz program, który przedstawia odległość przeszkody od czujnika na linijce diod LED. Im przeszkoda jest bliżej czujnika, tym więcej diod powinno się świecić. (...) Załóżmy, że doszliśmy do momentu, gdzie mamy wyznaczoną odległość od przeszkody, np. w zmiennej: long dist; // wyrażone w cm i chcemy napisać funkcję wizualizującą wartość na linijce 6 diod. Zadanie rozpocznijmy od deklaracji funkcji: void show_dist_on_bargraph(long distance_cm); Znowu przypominam, aby tworzyć nazwy funkcji i zmiennych, które same z siebie coś mówią po polsku może to być wyzwaniem, bo nie użyjemy polskich znaków, ale po angielsku "feel free". Teraz czas na definicję (opisanie ciała funkcji). Mamy 6 diod więc trzeba jakoś zawęzić nasze wartości. Musimy poczynić pewne założenia: załóżmy, że interesuje nas zakres od 10 do 40 cm, jeżeli wartość jest poza zakresem to ma przyjąć wartości graniczne: 10 albo 40. Do dzieła! Od razu dodajmy kilka stałych, żeby nie używać w kodzie magicznych liczb: #define MIN_DIST 10 #define MAX_DIST 40 void show_dist_on_bargraph(long distance_cm) { if (distance_cm < MIN_DIST) distance_cm = MIN_DIST; if (distance_cm > MAX_DIST) distance_cm = MAX_DIST; int lit_leds = map(distance_cm, MIN_DIST, MAX_DIST, 0, LEDS_COUNT); } Wewnątrz funkcji przekazywana jest domyślnie kopia zmiennej distance_cm (przekazanie przez wartość), więc możemy śmiało ją modyfikować. Uzyskana zmienna lit_leds informuje o indeksie zapalonej diody. Bez znajomości tablic dalsza część kodu może wyglądać w ten sposób: switch(lit_leds) { case 0: digitalWrite(LED_0, HIGH); digitalWrite(LED_1, LOW); digitalWrite(LED_2, LOW); digitalWrite(LED_3, LOW); digitalWrite(LED_4, LOW); digitalWrite(LED_5, LOW); break; case 1: digitalWrite(LED_0, LOW); digitalWrite(LED_1, HIGH); digitalWrite(LED_2, LOW); digitalWrite(LED_3, LOW); digitalWrite(LED_4, LOW); digitalWrite(LED_5, LOW); break; // ... } Już samo dokończenie pisania tego stworka budzi zastrzeżenia... spróbujmy to poprawić! Dysponując tablicą LED_pins zapalenie wybranej diody jest banalnie proste! for(int i = 0; i < LEDS_COUNT; i++) digitalWrite(LED_pins[i], LOW); digitalWrite(LED_pins[lit_leds], HIGH); I zrobione! Odległość jest reprezentowana przez punkt na linijce LED. Takie podejście ma też inną, bardzo ważną zaletę – jest elastyczne. Łatwo można zmienić kod tak by wyświetlać wszystkie LEDy do wybranego punktu włącznie, tzw. bargraph: for(int i = 0; i < LEDS_COUNT; i++) if(i < lit_leds) digitalWrite(LED_pins[i], HIGH); else digitalWrite(LED_pins[i], LOW); // lub for(int i = 0; i < LEDS_COUNT; i++) digitalWrite(LED_pins[i], i < lit_ledds ? HIGH : LOW); Czy nie ładniej? Jeżeli wiemy, że tablica zawiera niezmienne elementy możemy dodać przed typem danych słowo kluczowe const, w ten sposób stworzymy tablicę 6 stałych zabezpieczoną przed modyfikacją: const int LED_pins[] = {LED_0, LED_1, LED_2, LED_3, LED_4, LED_5}; Podsumowanie W tej części przybliżyłem temat tablic. Temat został poruszony od strony praktycznej, więc tę część polecam tym, którzy chcą rozwiązać jakieś zadanie i od razu zobaczyć efekty. W kolejnych częściach przyjrzymy się bardziej wnikliwym tematom. Edytowano Październik 29, 2021 przez Gieneq 5 3
Popularny post Gieneq Wrzesień 10, 2021 Autor tematu Popularny post Udostępnij Wrzesień 10, 2021 Pozwolę sobie podbić, bo kilka pierwszych części kursu pisałem jednocześnie i ta ma już prawie 2 tygodnie. Myślę, że temat ten będzie przydatny początkującym. Kolejne części będą nieco trudniejsze, ale podobnie jak w matematyce - bez dodawania niezabardzo da się nauczyć mnożenia i są tematy bez których nie ruszy się dalej. W razie niejasności jestem otwarty na sugestie, tak by materiał był możliwie dopracowany 3
Popularny post SOYER Luty 20, 2022 Popularny post Udostępnij Luty 20, 2022 @Gieneq dopiero teraz trafiłem na ten cykl. Bardzo dobrze napisane, wszystko ładnie wyjaśnione. Moim zdaniem powinniście to podwiesić jako kolejne odcinki drugiego kursu arduino. Brawo. 3
Gieneq Luty 21, 2022 Autor tematu Udostępnij Luty 21, 2022 @SOYER dziękuję, te pierwsze części przepisywałem kilka razy. Niestety nie dokończyłem, bo robię teraz coś innego ale wrócę do tematu. W planach są dość trudne zagadnienia i problem pojawił się w aktualnie ostatnim opublikowanym wpisie, który jest trochę niezrozumiały Ale dzięki, że napisałeś, mam więcej motywacji do dokończenia serii 1
Vuko Kwiecień 28, 2022 Udostępnij Kwiecień 28, 2022 Witam. To jest dokładnie to czego szukałem. Szczegółowe wyjaśnienie tablic na konkretnych przykładach! Jestem właśnie w trakcie realizacji projektu w którym dla czystości kodu aż się prosi o tablice. Także dzięki wielkie i liczę, że mój wpis będzie motywacją do dokończenia serii artykułów w tym temacie. Pozdrawiam! 1
Gieneq Kwiecień 29, 2022 Autor tematu Udostępnij Kwiecień 29, 2022 @Vuko @Krawi92 cieszę się, że cykl był przydatny. Przyznam, że trochę z braku jakiegoś większego feedbacku odechciało mi się kontynuować, ale jest to do zrobienia.
tenNowy Maj 16, 2022 Udostępnij Maj 16, 2022 Witam akurat natrafiłem na ten post swoją drogą ciekawy, ale mam pytanie. Chciałbym uzupełnić tablice stałymi za pomocą pętli for. Miałaby ona służyć do przechowywania stałych od A0 do A15 czyli zamiast pisać #define BUTTON1_PIN A0 #define BUTTON2_PIN A1 #define BUTTON3_PIN A2 // etc.. chcę mieć to w tablicy, którą uzupełnię pętlą i potem będę mógł z niej korzystać również za pomocą pętli ktoś zechciałby pomoc?
Gieneq Maj 17, 2022 Autor tematu Udostępnij Maj 17, 2022 @tenNowy ciekawy problem ale chyba nie da się go zrobić. Jakbyś chciał mieć pętlę w preprocesorze to się nie da, możesz zrobić to makrami na nasadzie rekurencji ale to nie pomoże. Pomyślałem że można złożyć tokeny konkatanacją i włożyć je do tablicy i później na wyjściu funkcji dać jako stałą, ale tego też się nie da: #include <stdio.h> #define A0 1 #define A1 2 #define A2 4 #define A3 3 #define A4 8 #define mkstr(str) #str #define cat(x,y) (x ## y) #define N 5 int* somestuff() { int results[N]; for(int i = 0; i < N; i++){ results[i] = cat(A, i); } } int main() { const int pins[N] = somestuff(); for(int i = 0; i < N; i++){ printf("%d : %d", i, pins); } return 0; } bo macro zwraca token Ai - tu sam wpadłem w pułapkę próby użycia pętli. Dla ciebie na pewno przydatna informacja że const to informacja że coś jest read-only więc możesz zrobić w czasie działania dane które mają później być tylko odczytywane - czyli tak jak użycie tej funkcji która zwraca wartości a ty uznajesz że będziesz je tylko czytał.
tenNowy Maj 17, 2022 Udostępnij Maj 17, 2022 Probowałem konkatenacji ale w każdym przypadku trzeba określić typ, jak się domyślasz pewnie potrzebne mi to do Arduino i chyba zrobię to bez pętli tylko potem przy ustawianiu pinmode również pętli nie użyje co za tym idzie brak jednej pętli ciągnie za sobą brak możliwości użycia jej w dalszej części kodu i robi się spagetti skoro mam 32 włączniki i tyleż samo przekaźników
farmaceuta Maj 17, 2022 Udostępnij Maj 17, 2022 No to przeciez mozesz uzyc normalnie tablicy w petli for i operowac indeksem, lud dodatkowo porobic stale nazwy i odwolywac sie przez ta tablice tymi nazwami zeby bylo wygodniej widziec jakim guzikiem/przekaznikiem operujesz..
Dantey Maj 17, 2022 Udostępnij Maj 17, 2022 @tenNowy A czemu musisz używać nazwy A0-A15? Rozważ też enuma jeśli chodzi o definiowane nazwy jakiś pinów
trainee Maj 17, 2022 Udostępnij Maj 17, 2022 @tenNowy, brakuje mi tutaj jednak odrobinę więcej informacji, co naprawdę próbujesz zrobić. Np. te makra An sugerowałyby, że potrzebujesz pinów do wejścia analogowego. Piszesz o pinMode(), co w wypadku wejścia analogowego nie jest niezbędne. Jednak Twoje makra nazywają się BUTTONn_PIN i wspominasz o włącznikach i przekaźnikach, a to brzmi już na wykorzystanie cyfrowe. Przy takim braku kontekstu nie jestem pewien czy jest sens skupiać się na tym jak próbujesz osiągnąć cel, skoro nie wiadomo co chcesz osiągnąć, bo mogłoby się okazać, że to jak niekoniecznie jest tym, którego potrzebujesz. 1
tenNowy Maj 18, 2022 Udostępnij Maj 18, 2022 Od D4 do D36 mam przekaźniki od A0 do A15 i od D37 do D52 włączniki ot to co chce osiągnąć za pomocą pętli
Pomocna odpowiedź
Bądź aktywny - zaloguj się lub utwórz konto!
Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony
Utwórz konto w ~20 sekund!
Zarejestruj nowe konto, to proste!
Zarejestruj się »Zaloguj się
Posiadasz własne konto? Użyj go!
Zaloguj się »