Skocz do zawartości

Przeszukaj forum

Pokazywanie wyników dla tagów 'C++'.

  • Szukaj wg tagów

    Wpisz tagi, oddzielając przecinkami.
  • Szukaj wg autora

Typ zawartości


Kategorie forum

  • Elektronika i programowanie
    • Elektronika
    • Arduino i ESP
    • Mikrokontrolery
    • Raspberry Pi
    • Inne komputery jednopłytkowe
    • Układy programowalne
    • Programowanie
    • Zasilanie
  • Artykuły, projekty, DIY
    • Artykuły redakcji (blog)
    • Artykuły użytkowników
    • Projekty - DIY
    • Projekty - DIY roboty
    • Projekty - DIY (mini)
    • Projekty - DIY (początkujący)
    • Projekty - DIY w budowie (worklogi)
    • Wiadomości
  • Pozostałe
    • Oprogramowanie CAD
    • Druk 3D
    • Napędy
    • Mechanika
    • Zawody/Konkursy/Wydarzenia
    • Sprzedam/Kupię/Zamienię/Praca
    • Inne
  • Ogólne
    • Ogłoszenia organizacyjne
    • Dyskusje o FORBOT.pl
    • Na luzie

Kategorie

  • Quizy o elektronice
  • Quizy do kursu elektroniki I
  • Quizy do kursu elektroniki II
  • Quizy do kursów Arduino
  • Quizy do kursu STM32L4
  • Quizy do pozostałych kursów

Szukaj wyników w...

Znajdź wyniki, które zawierają...


Data utworzenia

  • Rozpocznij

    Koniec


Ostatnia aktualizacja

  • Rozpocznij

    Koniec


Filtruj po ilości...

Data dołączenia

  • Rozpocznij

    Koniec


Grupa


Imię


Strona

Znaleziono 24 wyników

  1. Już myślałem, że mam ogarnięty temat RS-485. Jednak nie i znów potrzebuję pomocy. Czy są jakieś zasady / dobre praktyki, które należy stosować do obsługi komunikacji dwukierunkowej w trybie half duplex? Ew. ktoś to robił i może mi pomóc z moją aplikacją (np. jakiś wieczorny calll?) Aktualnie robię w miarę prosty układ obsługujący komunikację RS-485, wykorzystując 2 płytki: UNO R4 i GIGA. Całość ma działać tak: 1. (działa) Wciskam przycisk podpięty do UNO. UNO wysyła komunikat do GIGA. Kod: void sendReadMessage(int command) { char message[BatteryMonitor_Message_Lenght]; int commandValueParameter = 1; //wg przykładów z instrukcji urządzenia każdy read wysyła wartość = 1. int checksum = getChecksum(commandValueParameter); sprintf(message, ":R%02i=%i,%i,%i,\r\n", command, batteryMonitorParameters.deviceAddress, checksum, commandValueParameter); //Active HIGH for Driver Input Enable; Active LOW for Receiver Output Enable. digitalWrite(receiverOutputEnablePinNumber, HIGH); batteryMonitorSerialDevice->print(message); batteryMonitorSerialDevice->flush(); delay(WaitTimeAfterSend); //Active HIGH for Driver Input Enable; Active LOW for Receiver Output Enable. digitalWrite(receiverOutputEnablePinNumber, LOW); } W klasie instancją której operuję jest definicja zmiennej: Stream* batteryMonitorSerialDevice; a przy inicjowaniu podpinam pod nią Serial1 na którym jest poprawnie wpięty konwerter RS-485. 2. (działa) GIGA odbiera i prasuje komunikat. W zależności od treści otrzymanego i przetworzonego komunikatu ustala odpowiedź do wysyłki. Kod: void loop() { returnMessage = ""; switch (readMessage()) { case 0: { returnMessage = ":r00=1,47,1120,100,101,\r\n"; break; } case 50: { returnMessage = ":r50=2,215,2056,200,5408,4592,9437,14353,134,0,0,0,162,30682,\r\n"; break; } default: break; } if (returnMessage.length() > 0) { sendMessage(returnMessage); } delay(100); } int readMessage() { String returnMessage_local = ""; char message[BatteryMonitor_Message_Lenght]; int startIndex = 0; int endIndex = 0; bool receivedFullMessage = false; bool receivedMessage = false; int index = 0; while (Serial1.available() > 0) { message[index] = Serial1.read(); receivedMessage = true; if (message[index] == '\n') { receivedFullMessage = true; break; } index++; } if (receivedMessage and receivedFullMessage) { //parsuję wiadomość i szukam interesującego mnie kodu operacji for (int i = 0; i <= index; i++) { if (message[i] == 'R') { startIndex = i + 1; } if (message[i] == '=') { endIndex = i - 1; i = index + 1; //leave the loop } } for (int i = startIndex; i <= endIndex; i++) { returnMessage_local += message[i]; } if (returnMessage_local == "") { return -1; } else { return returnMessage_local.toInt(); //kod operacji } } return -1; } 3. (zaczynają się problemy) GIGA wysyła komunikat po RS-485. Nie mam żadnych oznak niepoprawnego działania. Kod: void sendMessage(String message) { message.toCharArray(messageToSend, BatteryMonitor_Message_Lenght); digitalWrite(ReceiverOutputEnablePinNumber, HIGH); for (int i = 0; i < message.length(); i++) { Serial1.write(messageToSend[i]); } Serial1.flush(); delay(100); digitalWrite(ReceiverOutputEnablePinNumber, LOW); } 4. (nie działa) UNO czeka na przychodzący komunikat. Jeżeli coś się pojawia to odbiera i wyświetla na serial (USB). Niestety przy kolejnych naciśnięciach przycisku albo nic nie odbiera, albo odbiera śmieci. Ani razu nie dotarła wiadomość zwrócona przez GIGA. void loop() { if (digitalRead(5) == LOW) { //Jeśli przycisk jest wciśnięty readBasicInformation(); SerialDebug_BM_BasicInformation(); //na potrzeby debugowania wyświetla otrzymane informacje na Serial / USB } delay(150); } void readBasicInformation() { char returnMessage[140]; String complexField = ""; char nextCharacter; int index = 0; //funkcja wysyłania z pkt 1 – ta działająca. Na wejściu kod operacji sendReadMessage(BatteryMonitor_Functions_ReadBasicInfo); //wiadomości są zakończone dwoma znakami: \r\n int tmpTimeout = 0; //czekam na wiadomość. Aby nie zawiesić aplikacji symulacja timeout w oczekiwaniu na komunikat. Kod do refactoringu while (batteryMonitorSerialDevice->available() <=0 && tmpTimeout <=30) { delay(150); tmpTimeout++; } //odczyt znak po znaku i wyświetlanie na Serial w celu debugowania while (batteryMonitorSerialDevice->available() > 0) { //tu dostaję śmieci lub nic nie dostaję…. returnMessage[index] = batteryMonitorSerialDevice->read(); Serial.print(returnMessage[index]); index++; } //odczyt wyświetlanie na Serial w celu debugowania Serial.print("Return message full: "); Serial.println(returnMessage); } Co mogę robić źle?
  2. 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: 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.
  3. Cześć, od jakiegoś czasu zaczałem się interesować językami, które mogłyby zastapić język C w programowaniu embedded. Chodzi mi o języki które są równie wydajne co C i mają nowoczesną składnię oraz ułatwiają pisanie kodu pozbawionego kilku błędów łatwych do popełnienia w języku C (zarządzanie pamięcią). W C++ według mnie zarządzanie pamięcią dla typów alokowanych na stercie jest lepsze niż w C - np. uzycie inteligentnych wskaźników i używanie kontenerów z biblioteki STL, ale sam język jest bardziej złożony niż C. Pierwszym językiem którym się zainteresowałem był Rust zapewniający większe bezpieczeństowo zarządzania pamięcią i programowania wielowątkowego. Potem trafiłem na język Zig, który zafascynował mnie prostą składnią i wydajnością często duzo wiekszą niż język C. Podaję tutaj linki do stron domowych dla języków Rust i Zig: https://www.rust-lang.org/ https://ziglang.org/ Czy ktoś ma doświadczenie w programowaniu mikrokontrolerów w jednym (lub obu) z tych języków i móglby się podzielić opiniami o ich przydatności i różnicach. Pozdrawiam
  4. Cześć piszę własną bibliotekę, trochę na bazie HAL ale uproszczoną i zoptymalizowaną, język: C++. W pliku serial.cpp zdefiniowałem klasę SerialInterface z podstawową obsługą portu, to jest OK - ślady kodu są widoczne w wynikowym kodzie (plik .list) /* USER CODE BEGIN WHILE */ SerialInterface tty0(USART2, 9600); 800022a: 463b mov r3, r7 800022c: f44f 5216 mov.w r2, #9600 ; 0x2580 8000230: 4904 ldr r1, [pc, #16] ; (8000244 <main+0x2c>) 8000232: 4618 mov r0, r3 8000234: f001 fa06 bl 8001644 <_ZN15SerialInterfaceC1EP13USART_TypeDefm> while (1){ // if (tty0.getReceived()>5) // tty0.sendByte('X'); // else if (tty0.getReceived()>0) // tty0.sendByte('L'); tty0.sendByte('-'); 8000238: 463b mov r3, r7 800023a: 212d movs r1, #45 ; 0x2d 800023c: 4618 mov r0, r3 800023e: f001 fa3f bl 80016c0 <_ZN15SerialInterface8sendByteEh> 8000242: e7f9 b.n 8000238 <main+0x20> Oprócz klasy, w pliku serial.cpp zdefiniowałem obsługę przerwania, funkcję USART2_IRQHandler jako: void USART2_IRQHandler(void){....} no i ta funkcja nie pojawia się już w listingu nie pojawia. Program testowy w pętli wysyła znaki, zgodnie z oczekiwaniem: (...) SerialInterface tty0(USART2, 9600); while (1){ tty0.sendByte('-'); } /* USER CODE END 3 */ } (...) jednak po odebraniu znaku "z zewnątrz" wysypuje się -> na debuggerze widzę, że następuje skok do obsługi nieznanego przerwania (👉Default_Handler), czyli faktycznie jakby linker połączył tylko część mojej biblioteki?? (plik startup_stm32g431kbtx.s w którym "ląduje" procesor, zawartość jest defaultowa, nie grzebane) : /** * @brief This is the code that gets called when the processor receives an * unexpected interrupt. This simply enters an infinite loop, preserving * the system state for examination by a debugger. * * @param None * @retval : None */ .section .text.Default_Handler,"ax",%progbits Default_Handler: Infinite_Loop: b Infinite_Loop <--- SKOK TUTAJ .size Default_Handler, .-Default_Handler Jak spowodować, aby linker dołączył moją obsługę IRQ? W CubeMX nie wyklikałem konfiguracji, żeby nie tworzył mi swoich "śmieci" z HAL, które są dalekie od optymalnych. Obsługa przerwania w oryginalnym HAL trwa wieki, a piszę aplikację wymagającą bardzo krótkich i sprawnych przerwań. Ktoś poradzi?
  5. Zastanawiam się na ile dużym problemem jest używanie dynamicznej alokacji do np. std::string, std::vector w embedded. Podobno fragmentacja może namieszać ale w jeszcze nie widzę jej skutków. Zamierzam zrobić coś z profilerem ale na to przyjdzie czas. Jednym ze sposobów poradzenia sobie z tym jest memory pool ale nie mam z tym większego doświadczenia. Np w ThreadX do założenia wątku, albo kolejki potrzeba przydzielić ręcznie pamięć, tu statycznie: #if (USE_STATIC_ALLOCATION == 1) UINT status = TX_SUCCESS; VOID *memory_ptr; if (tx_byte_pool_create(&tx_app_byte_pool, "Tx App memory pool", tx_byte_pool_buffer, TX_APP_MEM_POOL_SIZE) != TX_SUCCESS) { /* USER CODE BEGIN TX_Byte_Pool_Error */ Error_Handler(); /* USER CODE END TX_Byte_Pool_Error */ } else { /* USER CODE BEGIN TX_Byte_Pool_Success */ /* USER CODE END TX_Byte_Pool_Success */ memory_ptr = (VOID *)&tx_app_byte_pool; status = App_ThreadX_Init(memory_ptr); if (status != TX_SUCCESS) { /* USER CODE BEGIN App_ThreadX_Init_Error */ Error_Handler(); /* USER CODE END App_ThreadX_Init_Error */ } /* USER CODE BEGIN App_ThreadX_Init_Success */ /* USER CODE END App_ThreadX_Init_Success */ } A jak to może wyglądać z alokacją wspomnianych stringów czy wektorów? W mojej aplikacji przetwarzam stringi JSON i dynamiczne struktury są niezbędne. Nie jestem też w stanie określić jak duże sądane wejściowe więc statyczna alokacja bufora może się nie udać.
  6. Cześć, mając przetwornik A/C (np. MCP3426) na wyjściu otrzymuję liczbę stałoprzecinkową. Co, jeśli dodam do siebie 4 wyników pomiarów i podzielę ich sumę przez 4. Może się zdarzyć tak, że wynik będzie zmiennoprzecinkowy. Tu, już wchodzimy na arytmetykę zmiennoprzecinkową, często wolniejszą dla mikrokontrolera. Gdy patrzę na ten problem, zastanawiam się, czy jest sens w ogóle uśredniać w takim wypadku. Jak to rozwiązujecie?
  7. Cześć. Postanowiłem zamieścić tu przykład kodu w ramach podzielenia się swoja "twórczością". Jeśli zły dział, proszę przenieść. Post głównie w celach edukacyjnych dla osób, które jeszcze nie miały okazje zrezygnować z miliona zmiennych Pin_1, Pin_2 itd. Dużo przyjemniej i czytelniej jest mieć wszystko skompresowane do obiektów, nawet jeśli to bedzie przerost formy nad treścią. Po prostu sam chciałbym coś takiego przeczytać rok-dwa temu 🙂 Swego czasu, gdy trochę więcej poświęcałem czasu na embedded, natrafiłem na jednym z kursów M. Salamona fajną (według mnie) metodę implementacji obsługi przycisku. Postanowiłem sobie ją trochę przerobić, gdyż oryginał był pisany pod C i używał callbacków, które średnio mi dobrze pasowały, bo za każdym razem napotykałem rozmaite problemy, z którymi średnio sobie umiałem poradzić. Napisałem więc sobie to jako klasę, a kolejno dopisałem drugą klasę, która dziedziczy po niej, a jednocześnie implementuje to co dzieje się po wciśnięciu tego przycisku. Jest to taki chyba najprostszy przykład zastosowania programowania obiektowego dla arduino. Zamieszczam Link do GitHub z plikami .h i .cpp. gdyż wstawienie tu całego kodu byłoby trochę przesadą? Celem w ogóle użycia dziedziczenia było zrobienie swoistej "templatki" dla klasy, która implementuje obsługę przycisku, ale nie definiuje tego co dzieje się po wciśnięciu tego przycisku. Zapewne 80% przycisków w projektach po prostu wystawia jakiś stan na konkretny Pin, więc można było darować sobie to dziedziczenie, no ale.. zrobiłem sobie to głównie pod siebie na przyszłość. W temacie programowania obiektowego jestem początkujący więc wszelakie uwagi od bardziej doświadczonych kolegów mile widziane, bo niestety ale wszystko to moje własne próby sklejenia czegoś z informacji, które zdobyłem. Opisując skromnie co dzieje się w implementacji i jakie są założenia: 1. Brak możliwości stworzenia podstawowej klasy // virtual destructor virtual ~TypeButton() = 0; // virtual methods virtual void ButtonPressed() = 0; virtual void ButtonNotPressed() = 0; Chciałem stworzyć sobie klasę, która będzie miała zaimplementowaną swoista "blokade", żeby nie kusiło stworzenia obiektu, który nie posiada implementacji dla funkcji obsługującej przycisk. W tym celu zmieniłem destruktor na wirtualny i przypisałem mu 0. W ten sposób ustawiłem destruktor czysto wirtualny, czyli taki którego nie można wywołać. Co za tym idzie, nie można również stworzyć obiektu. 2. Funkcje Pressed i NotPressed są wykonywane raz Funkcje wykonywane są raz tylko przy zmianie stanów z Debbounce -> Pressed i z Pressed-> Idle. Zabezpieczało mnie to przed wchodzeniem ciągle, w funkcje, która ustawia stan na wysoki/niski. 3. Działanie głównie opiera się na sprawdzaniu w pętli metody ButtonTask() void TypeButton::ButtonTask() { switch (this->State) { case IDLE: ButtonIdle(); break; case DEBBOUNCE: ButtonDebbounce(); break; case PRESSED: ButtonIsPressed(); break; default: break; } } Metoda za każdym razem sprawdza aktualny stan obiektu, z którego została wywoływana (this->State) i wykonuje funkcje, zależnie od tego w jakim stanie znajduje się aktualnie przycisk. 4. Opierając się na Klasie-rodzic można dorabiać dalej własne implementacje klasy-dziecka, które bedą wykonywać inne zadania. Kwestia inwencji własnej. Ogólnie, jak wspomniałem wcześniej wygląda to jak przerost formy nad treścią, ale obecnie bardzo mi ułatwia pisanie prototypów bo nie musze znowu martwić czy dobrze napisałem kod do przycisku. No i było bardzo fajną lekcją w kontekście robienia klas/metod wirtualnych. Z napotkanych problemów: Mimo, że zaimplementowałem destruktor jako pure virtual to i tak musiałem dodać w pliku .cpp na początku: TypeButton::~TypeButton(){} Bez tego dostawałem ciągłe błedy o tym, że w klasie-dziecko jest nieznany typ destruktora własnie TypeButton::~TypeButton(). Trochę nie mogłem zrozumieć czemu musiałem to zrobić. Wygrzebałem to w jednym z tematów na Stack Overflow i było to tam opisane czymś w rodzaju "Czasem trzeba dodać tego typu deklaracje mimo pure virutal destruktora". Albo ja nie uważałem podczas uczenia się, ale rzadko kiedy widziałem by ktoś miał taki problem. Może ktoś dużo mądrzejszy mi to wytłumaczy? 🙂 To by było na tyle. Wszelakie uwagi mile widziane
  8. Udało Ci się napisać pierwsze programy z wykorzystaniem tablic. Ta część kursu jest bardziej teoretyczna, ale pozwoli Ci zapoznać się z organizacją pamięci w mikrokontrolerze ATmega328P. Dowiesz się o różnych typach zmiennych i gdzie są zapisywane. Przyda się to w nabraniu świadomości jak działa tablica. Spis treści #1 – wstęp i praktyka #2 – organizacja pamięci (czytasz ten artykuł) #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 Język Arduino? Zanim przejdę do sedna sprawy zadam szybkie pytanie: W jakim języku programuje się Arduino? Nie jest to takie oczywiste, bo często spotyka się zapis C/C++ lub informację "dialekt C++". Są to poprawne informacje, ale aby być bardzie szczegółowym, wystarczy sprawdzić co znajduje się w pliku związanym z kompilacją. Zakładając, że używamy Arduino z mikrokontrolerem AVR ATmega328P przechodzimy do katalogu np. C:\Program Files (x86)\Arduino\hardware\arduino\avr i tam szukamy pliku związanego z biblioteką płytek pobranej w menadżerze płytek. Wewnątrz pliku platform.txt znajdziemy blok okomentowany jako domyślne ustawienia, gdzie jedna z linii informuje nas o użyciu C++11: compiler.cpp.flags=-c -g -Os {compiler.warning_flags} -std=gnu++11 -fpermissive -fno-exceptions -ffunction-sections -fdata-sections -fno-threadsafe-statics -Wno-error=narrowing -MMD -flto Podobną informację znajdziemy w logu kompilacji z Arduino IDE. Jeszcze jednym ciekawym sposobem jest wypisanie predefiniowanej nazwy preprocesora __cplusplus identyfikującej standerd C++. Wypisanie liczby 201103 będzie oznaczać C++11: Serial.println(__cplusplus); Wiedząc to możemy być bardzie świadomi co nam wolno, a czego nie Przykładowo, niektórzy zwolennicy czystego C będą na pewno wystrzegać się klas, wydedukowanego typu danych auto, wyrażeń lambda i innych zdobyczy "zinkrementowanego C", na których bazuje część bibliotek Arduino. Na szczęście tablice występują w C więc nie jest to temat dyskusyjny, ale w ramach kursu skorzystamy z innych dobrodziejstw C++11. Szczegóły dotyczące języka i procesu kompilacji dostępne są w osobnej serii artykułów. Optymalizacja Domyślnie w Arduino IDE włączona jest optymalizacja rozmiaru pliku wynikowego -Os. Jest to potrzebne dla mikrokontrolerów o ograniczonych zasobach (ATmega328P posiada tylko 2 kB RAM). Może to jednak powodować problemy zwłaszcza, gdy eksperymentujemy z pamięciĄ, więc tymczasowo polecam wyłączyć optymalizację podmieniając w pliku platform.txt -Os na -O0: Później warto jednak do niej wrócić, optymalizacja rozmiaru -Os może zmniejszyć kod programu nawet o ponad 80%! Organizacja pamięci operacyjnej Aby lepiej zrozumieć dalszą treść, niezbędna jest podstawowa wiedza na temat organizacji pamięci operacyjnej RAM. Jak wiemy mikrokontroler ATmega328P posiada 2 kB pamięci SRAM. Przeglądając 18 stronę dokumentacji znajdziemy diagram: wynika z niego, że pamięć operacyjna zaczyna się w adresie 0x0100, a kończy w adresie 0x08FF. Oznacza to, że mamy do dyspozycji 2048 bajtów pamięci, czyli tyle ile zadeklarował producent: 2 kB (albo bardziej jednoznacznie 2 KiB). Pamięć ta jest dalej dzielona na obszary przeznaczone do konkretnych zadań: Jak możemy zauważyć zaznaczone są tu 4 istotne obszary: sekcja .data, sekcja .bss, sterta (ang. heap), stos (ang. stack) . Dygresja o wskaźnikach Niestety bez tego nie przejdziemy dalej. Wskaźniki straszą początkujących, ale kiedyś trzeba je polubić, bo bez nich za daleko się nie zajedzie. Na szczęście dla potrzeb poznania adresów zmiennych nie będziemy potrzebować zbyt wiele. Wskaźnik na zmienną służy do wskazywania... czyli pokazywania, gdzie w pamięci znajduje się zmienna. Informacja gdzie jest dana zmienna zawarta jest w adresie (typie danych wskaźnika). To wszystko? W zasadzie tak 🙂 Działa to na podobnej zasadzie co pocztowe dane adresowe. Przykładowo, zdefiniujmy zmienną typu int o nazwie velocity i przypiszmy jej wartość 12: int velocity = 12; // alternatywny zapis: int velocity {12}; To co się wydarzyło to zarezerwowanie w pamięci obszaru, gdzie zapisana została wartość 12. Jak duży to obszar? Z kursu Arduino dowiedziałeś się, że różne typy danych charakteryzują się różną wielkością (liczoną w bajtach) i przeznaczeniem. Zmienne typu int na tym mikrokontrolerze zajmują w pamięci 2 B i można to sprawdzić operatorem sizeof(). Serial.println(sizeof(velocity)); W tym przypadku użyliśmy operatora na zmiennej, ale można też użyć na samym typie danych: Serial.print("Size of char: "); Serial.println(sizeof(char)); Serial.print("Size of int: "); Serial.println(sizeof(int)); Serial.print("Size of long: "); Serial.println(sizeof(long)); Serial.print("Size of float: "); Serial.println(sizeof(float)); W wyniku otrzymamy: Size of char: 1 Size of int: 2 Size of long: 4 Size of float: 4 Dobrze wiemy jak sprawdzić ile pamięci zajmują zmienne, ale gdzie te wskaźniki? Śpieszę z wyjaśnieniem. Przypatrzmy się jeszcze raz zmiennej velocity. Wiemy, że zajmuje 2 B pamięci. Aby uzyskać adres tego obszaru pamięci użyjmy jednoargumentowego operatora & udostępniającego adres: int *address_velocity = &velocity; A co to za stwór po lewej stronie? Tak wygląda typ danych skojarzony ze wskaźnikiem na zmienną int. Choć gwiazdkę stawia się przy nazwie zmiennej to typ danych to int * – tak się przyjęło (choć możesz spotkać też inne zapisy – ze spacją lub bez niej). Przykładowo w środowisku Visual Studio Code dostępne są do wyboru wszystkie sposoby zapisu: Dla innych typów danych będą inne skojarzone z nimi wskaźniki – dla long będzie long *, dla char będzie char * itd. Skoro jest to tylko adres, to dlaczego istnieją osobne typy danych, a nie jeden dla adresów? Dzieje się tak, gdyż wskaźnik niesie też dodatkową informację, np. o tym jak wiele miejsca zajmuje zmienna, co przyda się, gdy wrócimy do tematu tablic. Istnieje uniwersalny wskaźnik, który wskazuje obszar pamięci, ale nie posiada informacji o rozmiarze danych: void *. Aby poznać adres wskaźnika możemy zrzutować go na int i wyświetlić w monitorze portu szeregowego: int velocity = 12; int *address_velocity = &velocity; void setup() { Serial.begin(9600); Serial.print(F("0x")); Serial.println((int)address_velocity, HEX); } Ważne jest aby wypisać liczbę w systemie heksadecymalnym (HEX). W wyniku otrzymamy adres zmiennej: 0x100 Dlaczego akurat taki adres, o tym za chwilę. Na razie zakończę temat wskaźników jeszcze jedną informacją – dysponując wskaźnikiem można uzyskać wartość, na którą wskazuje (obszar pamięci pod adresem wskaźnika). Służy do tego jednoargumentowy operator *, który używamy następująco: Serial.println(*address_velocity); W wyniku z powrotem otrzymamy wartość 12. Kapitanie... do brzegu – obszary pamięci Tak, tak... już już wracam do tematu 🙂 Wiemy jak sprawdzić adres zmiennej w pamięci i wiemy, że są jakieś obszary pamięci: .data, .bss, heap i stack. Co dokładnie oznaczają? .data – ten obszar pamięci jest na samym początku SRAM i zaczyna się od adresu 0x100 (adres ten zdążyliśmy już zauważyć w zmiennej predkosc). Sekcja .data służy do przechowywania danych statycznych to znaczy takich, które istnieją przez cały czas działania programu. Do tych zmiennych należą: zmienne globalne (te które zainicjalizowaliśmy "na górze" kodu) ale mające wartość inną niż zero (zero jest domyślnie wartością zmiennych niezainicjowanych... tak wiem, pokręcone), zmienne lokalne z słowem kluczowym static, czyli lokalne zmienne statyczne (też w wywołaniach funkcji) z przypisaną niezerową wartością. .bss – kolejny obszar służący do przechowywania danych statycznych ale niezainicjowanych lub posiadających wartość 0. Do tych zmiennych należą: zmienne globalne niezainicjowane lub o wartości 0, statyczne zmienne lokalne niezainicjowane lub o wartości 0. Uwaga! Zmienne statyczne to nie zmienne stałe, ich wartość może się zmieniać. Przykłady zmiennych statycznych: int velocity = 5; // globalna zmienna, zainicjowana, niezerowa – .data int momentum; // globalna zmienna, niezainicjowana, domyślnie wartość 0 – .bss void setup() { // wewnątrz funkcji – zmienne lokalne static int acceleration = 2; // statyczna zmienna lokalna, zainicjowana, niezerowa – .data static int steps_numer; // statyczna zmienna lokalna, niezainicjowana, domyślna wartość 0 – .bss steps_numer = 5; // niezerowa wartość, ale dalej w .bss } Zmienne statyczne mogą się przydać w sytuacji, gdy projektujemy funkcję, która powinna mieć zapamiętany jakiś swój wewnętrzny stan. Zamiast tworzyć zmienną globalną, która byłaby widoczna w wielu miejscach w kodzie i mogłaby być myląca, to można utworzyć lokalną zmienną statyczną, której wartość nie zostanie utracona nawet po wyjściu z funkcji, ale będzie widoczna tylko wewnątrz tej funkcji. Takie zachowanie zmiennej może być zaskakujące, ale dzieje się tak, gdyż adres obszaru pamięci jest na stałe zarezerwowany. Jeżeli utworzymy zmienną jedynie przez definicję, to jej początkowa wartość będzie wynosić 0 lecz przy kolejnych wywołaniach wartość będzie aktualizowana. Przykładowo następujący kod zawiera statyczną zmienną lokalną i intuicja podpowiada, że pewnie jej wartość zostanie utracona po wyjściu z funkcji. void use_counter() { static int count; Serial.print(F("Function used: ")); Serial.print(count++); Serial.println(F(" times.")); } void loop() { use_counter(); delay(1000); } Efekt działania jednak wskazuje, że zmienna nie jest wymazana: Function used: 0 times. Function used: 1 times. Function used: 2 times. Function used: 3 times. Function used: 4 times. Zmienne i funkcje statyczne na pewno spotkasz nieraz w plikach bibliotecznych, gdyż użycie ich poprawia hermetyzację danych (widoczność tylko w ograniczonym zakresie) i wymusza pierwszeństwo przy kompilacji. Nie można tego mylić z metodami statycznymi klas, gdzie static oznacza przynależność do klasy, a nie poszczególnych instancji. stack – specjalnie zmieniłem kolejność, bo stos jest bardziej intuicyjny. Jest to obszar pamięci dynamicznej (czyli takiej która jest zapełniania i zwalniana w czasie działania programu) zaczynający się od końca RAM i rosnący w stronę początku pamięci. Zapełnianie stosu następuje automatycznie gdy: wywoływane są funkcje, tworzone są zmienne lokalne. Dane są dopisywane jedna po drugiej, stąd są one ciasno upchane (nie posiada luk). Posługując się przykładem z ilustracji jest to podobne do poukładanej ścianki narzędziowej, gdzie pobieramy i odkładamy narzędzia wiszące obok siebie. Zwalnianie stosu następuje automatycznie wraz z zakończeniem wywołania funkcji. Przykład zmiennej zapisanej na stosie: void setup() { int local_variable = 5; } Uwaga! Zmienne lokalne zapisane na stosie domyślnie mają wartość losową. Dzieje się dlatego, że przy większej liczbie danych (np. w pętli) ustawianie każdorazowo wartości na 0 pogarszałoby czas wykonania. Przykładowo: int local_variable; Serial.print(F("0x")); Serial.print((int)&local_variable, HEX); Serial.print(F(", value: ")); Serial.println(local_variable); Jak widać adres zmiennej jest bardzo blisko końca: 0x8FF i wskazywana wartość jest zupełnie przypadkowa: 0x8F6, value: -29696 heap – sterta to również obszar pamięci dynamicznej ale przeznaczony do przechowywania danych, gdy tworzone są nowe obiekty (alokowana pamięć) w czasie działania programu. Dokonuje się tego: używając operatora new, który przydziela pamięć do obiektu lub tablicy, alokując blok pamięci funkcją malloc(). Różnica polega na tym, że obiekty te tworzy programista – dodawanie i ściąganie obiektów ze starty nie jest zautomatyzowane. Sterta jest bardziej chaotyczna, może posiadać luki (można zwolnić dowolny blok pamięci pozostawiając dziurę). Choć możliwa jest większa swoboda (np. rozszerzenie obszaru pamięci w tablicy dynamicznej) to może to wiązać się, z przeniesieniem bloku pamięci. W ogólności dostęp jest wolniejszy od obiektów ze stosu. Posługując się przykładem stołu warsztatowego, jest to blat na którym sporo się dzieje – w różnych miejscach dokładane i przenoszone są materiały i narzędzia. Więcej na temat zagrożeń używania pamięci dynamicznej w części kurs o tablicach znaków. Dobrym podsumowaniem będzie analiza adresów zmiennych z przykładu: int zm_glob_niezer = 12; int zm_glob_zer = 0; int zm_glob_niezainc; static int zm_glob_niezer_static = 18; static int zm_glob_zer_static = 0; void setup() { int zm_lok_niezer = 33; int zm_lok_niezain; static int zm_lok_zer_stat = 6; static int zm_lok_niezain_stat; String *napis = new String("3333"); Serial.begin(9600); Serial.print(F("0x")); Serial.print((int)&zm_glob_niezer, HEX); Serial.print(F(" - zm_glob_niezer, ")); Serial.println(zm_glob_niezer); Serial.print(F("0x")); Serial.print((int)&zm_glob_zer, HEX); Serial.print(F(" - zm_glob_zer, ")); Serial.println(zm_glob_zer); Serial.print(F("0x")); Serial.print((int)&zm_glob_niezainc, HEX); Serial.print(F(" - zm_glob_niezainc, ")); Serial.println(zm_glob_niezainc); Serial.print(F("0x")); Serial.print((int)&zm_glob_niezer_static, HEX); Serial.print(F(" - zm_glob_niezer_static, ")); Serial.println(zm_glob_niezer_static); Serial.print(F("0x")); Serial.print((int)&zm_glob_zer_static, HEX); Serial.print(F(" - zm_glob_zer_static, ")); Serial.println(zm_glob_zer_static); Serial.print(F("0x")); Serial.print((int)&zm_lok_niezer, HEX); Serial.print(F(" - zm_lok_niezer, ")); Serial.println(zm_lok_niezer); Serial.print(F("0x")); Serial.print((int)&zm_lok_niezain, HEX); Serial.print(F(" - zm_lok_niezain, ")); Serial.println(zm_lok_niezain); Serial.print(F("0x")); Serial.print((int)&zm_lok_zer_stat, HEX); Serial.print(F(" - zm_lok_zer_stat, ")); Serial.println(zm_lok_zer_stat); Serial.print(F("0x")); Serial.print((int)&zm_lok_niezain_stat, HEX); Serial.print(F(" - zm_lok_niezain_stat, ")); Serial.println(zm_lok_niezain_stat); Serial.print(F("0x")); Serial.print((int)napis, HEX); Serial.print(F(" - napis, ")); Serial.println(*napis); } void loop() { } W wyniku otrzymamy coś takiego: 0x100 - zm_glob_niezer, 12 0x1A4 - zm_glob_zer, 0 0x1A6 - zm_glob_niezainc, 0 0x102 - zm_glob_niezer_static, 18 0x1A8 - zm_glob_zer_static, 0 0x8F2 - zm_lok_niezer, 33 0x8F4 - zm_lok_niezain, -26879 0x104 - zm_lok_zer_stat, 6 0x1AA - zm_lok_niezain_stat, 0 0x25B - napis, 3333 Widzimy wyraźnie adresy zainicjowanych danych statycznych (wartości bliskie 0x100). Nieco większe wartości przyjmują dane z obszaru niezainicjowanych danych statycznych (.bss). Następnie widoczny jest spory skok adresowania w przypadku zmiennych lokalnych umieszczonych na stosie (w tym tych niezainicjowanych), gdzie została odczytana losowa wartość. Na sam koniec została zaalokowana pamięć w obszarze sterty przy pomocy operatora new, które mają adresy większe od tych z .bss. Do tego czym jest operator new jeszcze kiedyś wrócimy. Ląd na horyzoncie – gdzie te tablice? Dygresja od dygresji ale bez tego nie da się omówić tematu 😉 Do czego to wszystko ma się przydać? W kolejnej części zostanie omówiony sposób tworzenia tablic (statycznych), ale tym razem ze świadomością co się dokładnie dzieje, a gdy dojdziemy do tematu alokacji dynamicznej, tworzenia obiektów to zrozumiemy dlaczego ta sterta jest trochę niebezpieczna. Oczywiście, bez tego da się żyć, dlatego też pierwsza część jest typowo praktyczna. Sam przeżyłem bez znajomości tego tematu kilka lat, napisałem w tym czasie wiele programów, brałem udział nawet w konkurach z algorytmiki i coś wygrałem! Ale apetyt rośnie w miarę jedzenia i warto kiedyś pochylić się nad tym tematem 📚
  9. Mam dwie tablice char: char t1[100]; char t2[2048]; oraz wskaźnik który zależnie od potrzeb wskazuje na t1 lub t2 char * ptr; ptr = t1; czy jest możliwość aby za pomocą sizeof pobrać rozmiar tablicy na którą aktualnie ustawiony jest wskaźnik?
  10. W ostatniej części omówiłem typowe sposoby tworzenia tablic i skończyliśmy na omówieniu koncepcji skakania pomiędzy zmiennymi posługując się operacjami na wskaźnikach. W tej części wykorzystamy tę koncepcję i potraktujemy tablicę jako wskaźnik, dzięki czemu możliwe będzie przyspieszenie niektórych operacji. Spis treści #1 – wstęp i praktyka #2 – organizacja pamięci #3 – tablice w pamięci #4 – tablica jest wskaźnikiem (czytasz ten artykuł) #5 – znaki, cstring #6 – argumenty funkcji #7 – przykład #8 – tablice wielowymiarowe #9 – tablice dynamiczne #10 – zakończenie Adres tablicy W jednym z ostatnio omawianych przykładów, wypisaliśmy adresy elementów tablicy: #define LOCKERS_COUNT 5 int lockers[LOCKERS_COUNT] = {1, 0, 1}; void setup() { Serial.begin(9600); for (int i = 0; i < LOCKERS_COUNT; i++) { Serial.print(F("0x")); Serial.println((int)&lockers[i], HEX); } } W wyniku możemy otrzymać takie dane: 0x100 0x102 0x104 0x106 0x108 Nigdzie jednak nie podaliśmy jaki jest adres samej tablicy, dlatego pora na najważniejszą informację dotycząca tablic: Nazwa tablicy jest zarazem adresem jej początkowego (zerowego) elementu. Tablica nie jest typem danych wiec nie ma adresu, jej adresem jest adres zerowego elementu, a nazwa jako adres obszaru pamięci może być stosowana wymiennie. W praktyce oznacza to że oba zapisy są poprawne: int *addr_1 = lockers; int *addr_2 = &lockers[0]; Można sprawdzić czy adresy są faktycznie takie same wypisując je: 0x100 0x100 To samo a jednak różne Czy skoro uzyskaliśmy ten sam adres w pamięci, czy można dojść do wniosku, że nazwa tablicy i wskaźnik są tym samym? Choć w obu przypadkach uzyskamy informację o adresie, to nazwa tablicy jest nieco bogatsza w informacje. Prześledźmy to na przykładzie, tylko tym razem użyjemy tablicy 1 B zmiennych char: char tab[5] = {'a', 'b', 'c', 'd', 'e'}; void setup() { Serial.begin(9600); char *addr = tab; Serial.print(F("Sizeof(tab): ")); Serial.println(sizeof(tab)); Serial.print(F("Sizeof(addr): ")); Serial.println(sizeof(addr)); Serial.print(F("Sizeof(&tab[0]): ")); Serial.println(sizeof(&tab[0])); Serial.print(F("Sizeof(tab[0]): ")); Serial.println(sizeof(tab[0])); } Użyty tu operator sizeof() zazwyczaj działa na zmiennej lub jej typie (jawnie wpisanej nazwie typu danych) i zwraca rozmiar zmiennej. Jednak w przypadku tablicy jest nieco inaczej, bo operator zwróci rozmiar obszaru zajmowanego przez tablicę: Sizeof(tab): 5 Sizeof(addr): 2 Sizeof(&tab[0]): 2 Sizeof(tab[0]): 1 Uzyskana wartość 5 oznacza, że tablica 1 B elementów int zajmuje 5 × 1 B pamięci. Zatem pierwsza różnica polega na tym jakie są dodatkowe informacje. Tablica niesie informację o całym obszarze pamięci, a wskaźnik niesie informację tylko o adresie, stąd używając operatora sizeof() uzyskaliśmy rozmiar ale nie wskazywanej wartości, tylko zmiennej typu danych wskaźnika – adres 2 B (16 bitowy) – w końcu adres musi być jakoś przechowany w pamięci. Dysponując tablicą jesteśmy w stanie uzyskać rozmiar bloku pamięci, rozmiar typu danych i nawet odtworzyć rozmiar tablicy wyrażony w liczbie elementów: int tab[] = {1, 2, 3, 4, 5}; int tab_len = sizeof(tab) / sizeof(tab[0]); // wynik 5 lub posługując się makrem: #define ARRLEN(x) (sizeof(x)/sizeof(x[0])) int tab_len = ARRLEN(tab); W prostych zadaniach najpewniej rozmiar tablicy (zwłaszcza statycznej) będzie znany więc takie obliczenia będą niepotrzebne. Jest to jednak dobre ćwiczenie na użycie operatora sizeof(), który może się przydać w zadaniach, gdzie trzeba wyznaczyć rozmiar bloku pamięci. Przykładem może być fragment funkcji z biblioteki Arduino: size_t Print::printNumber(unsigned long n, uint8_t base) { char buf[8 * sizeof(long) + 1]; // Assumes 8-bit chars plus zero byte. char *str = &buf[sizeof(buf) - 1]; ... Jak widać operator sizeof() pozwala wyznaczyć rozmiar bufora zwalniając programistę z wiedzy o rozmiarze typu danych long. Innym przykładem użycia może być złożony obiekt jakim jest klasa wektor (taka nieco lepsza tablica dynamiczna). Nie dość, że jako szablon (ang. template) może przyjąć dowolny typ danych (w tym obiekty klas) to może zmieniać swój rozmiar w czasie działania. Implementacja tak złożonego obiektu nie mogłaby obyć się bez tej wiedzy. Druga różnica tyczy się próby zmiany adresu. Jak przekonaliśmy się w poprzedniej części da się dodać coś do wskaźnika i w konsekwencji odwołać się do innego adresu. Mając wskaźnik uzyskany z tablicy jest to możliwe: int *addr = tab; addr++; //ok Ale próbując tego samego na tablicy już się nie uda: tab++; // błąd Dzieje się tak ponieważ spowodowałoby to utratę informacji o faktycznym adresie tablicy. Zatem wracając do regułki z początku można ją wzbogacić uzyskując trafniejszą definicję: Nazwa tablicy jest zarazem niezmiennym adresem jej początkowego (zerowego) elementu. Warto znać różne sposoby. Nawet jeżeli sami ich tak szybko nie użyjemy, to przeglądając kody programów bibliotek na pewno trafimy na takie przypadki. Dlatego teraz przećwiczymy alternatywny sposób poruszania się po tablicach – przy pomocy adresów. Indeksowanie bez nawiasów Skoro tablica jest dobrodziejstwem, w którym możemy śmiało skakać wskaźnikami bez obawy, że (w obrębie tablicy) coś naruszymy to warto z tego skorzystać. Zdefiniujmy więc tablicę, adres do niej i przy pomocy pętli prześledźmy jej wartości: int tab[5] = {1, 2, 3, 4, 5}; int *addr = tab; void setup() { Serial.begin(9600); for (int i = 0; i < 5; i++) Serial.println(*addr++); } W wyniku otrzymamy to co podaliśmy przy inicjalizacji tablicy: 1 2 3 4 5 Przy inicjalizacji tablicy co prawda posługujemy się nawiasami kwadratowymi (choć da się inaczej), ale reszta jest już inna. Co właściwie oznacza ten zapis? Mamy tu pewne uproszczenie – w jednej linii realizujemy pobranie wskazywanej wartości i operację postinkrementacji (zwiększenie zmiennej, ale dopiero po odczytaniu jej wartości). Używając notacji z nawiasami kwadratowymi można posłużyć się zmienną i, czy w tym zapisie jest to możliwe? Tak, jak najbardziej: for (int i = 0; i < 5; i++) Serial.println(*(tab + i)); Dodanie czegoś do tablicy nie narusza jej niezmiennego adresu, zwracany wynik jest nowym adresem, z którego odczytujemy wskazywaną wartość. Na razie wygląda to jakbyśmy uczyli się dokładnie tego samego zapisu ale w trochę przekombinowany sposób. Okazuje się, że nie jest on taki wcale zbędny. Są sytuacje w których będzie on łatwiejszy i szybszy od odpowiednika z typowymi tablicami, zwłaszcza gdy będziemy chcieli odwołać się do pewnych obszarów tablicy i wykonać na nich działania. Dygresja o dodawaniu (i odejmowaniu też) Jak można było zauważyć posłużyłem się pojęciem postinkrementacji. Sama inkrementacja, czyli zwiększenie zmiennej o 1 jest szerszym pojęciem pod którym kryją się 2 operacje o nieco innym sposobie dojścia do tego samego efektu: preinkrementacja i postinkrementacja: preinkrementacja (++v) dokonuje bezpośredniego zwiększenia zmiennej, w efekcie przyległe operacje działają na zwiększonej zmiennej, postinkrementacja (v++) dokonuje zwiększenia zmiennej, ale na tymczasowej kopii, w efekcie przyległe operacje działają na wartości oryginalnej (niezwiększonej), a po zakończeniu działań zmienna jest modyfikowana. Do czego to może służyć? Tak jak w poprzednim przykładzie, świadomie wypisaliśmy zmienną, aby w tej samej linii ją zwiększyć, ale dopiero po wykonaniu przyległej operacji dereferencji *. Poza kwestią funkcjonalną, operacje te różnią się szybkością wykonania, ta pierwsza z racji bezpośredniego działania na pamięci jest szybsza. Czy zatem pisanie ++i jest szybsze niż i++ w pętli for? Nie... jeżeli mamy włączoną optymalizację. W przypadku pętli for kolejność inkrementacji nie jest tak istotna, bo nie odwołujemy się podczas niej do wartości. Kompilator zoptymalizuje te działanie do szybszej wersji. Podobnie samodzielna operacja postinkrementacji zostanie zoptymalizowana do szybszego wariantu. Oczywiście na podobnej zasadzie działają operacje dekrementacji (zmniejszania). Porównanie wskaźników Skoro wiemy, że wskaźnik jest w 8 bitowych mikrokontrolerach AVR liczbą 16 bitową, to pewnie inne operacje na wskaźnikach też są możliwe. Bez problemu możemy użyć operatora porównania == a także różne warianty operatorów nierówności. Prześledźmy to na przykładzie: int tab[5] = {1, 2, 3, 4, 5}; int *addr = tab; void setup() { Serial.begin(9600); Serial.println(tab == &tab[0] ? F("Equal") : F("Different")); for (int *addr = tab; addr < &tab[4]; addr++) Serial.println(*addr); } W wyniku otrzymamy: Equal 1 2 3 4 W pierwszej linii wypisaliśmy wynik porównania dwóch wskaźników i jak już wiemy wskaźnik od nazwy tablicy i wskaźnik elementu zerowego są sobie równe. W pętli for przeprowadziliśmy iterację posługując się wskaźnikiem w miejscu typowej zmiennej int i. Jak widać przy każdym obiegu pętli następuje sprawdzenie, czy adres jest mniejszy od adresu ostatniego elementu w tablicy. Stąd wniosek – adresy można ze sobą porównywać. Posługując się przykładem to tak jakbyśmy szli wzdłuż ulicy i szukali konkretnego budynku. Widząc budynki o niższych adresach wiemy, że powinniśmy iść zgodnie z rosnącymi adresami i odwrotnie. Podsumowanie W tej części dotknęliśmy bardzo ciekawy temat, który często budzi strach u początkujących programistów. Przemieszanie gwiazdek, ampersandów, plusów i minusów już nie powinien stanowić problemu. W kolejnych częściach przejdziemy do bardzo popularnego zastosowania tablic – przechowywania ciągów znaków (napisów) czyli tzw. stringów.
  11. Zapraszamy do współpracy osoby, posiadające doświadczenie w pracy z Arduino, pasjonujące się nowoczesnymi technologiami i lubiącymi pracę z dziećmi. Robocode to młoda, progresywna firmą, oferująca szkolenia dla dzieci i młodzieży w wieku od 8 do 16 lat w zakresie robotyki, programowania i myślenia projektowego 🚀 🤖 W związku z otwarciem pierwszej w Katowicach szkoły poszukujemy nauczycieli/instruktorów zajęć, pragnących rozwijać ją razem z nami! Nauczyciel w Robocode odpowiada za: Przeprowadzanie zajęć w interesujący i zrozumiały sposób, Znalezienie wspólnego języka z dziećmi, Śledzenie trendów innowacji w IT, Bieżącą współpracę z menedżerem szkoły i zespołem. 😎 Instruktor Robocode jest ekspertem, przyjacielem i mentorem, więc jeśli czujesz, że chodzi o Ciebie, jeśli lubisz dzielić się wiedzą oraz jesteś aktywną, dobrze zorganizowaną i nastawioną na rozwój osobą, dołącz do nas! Zapewniamy niezbędne materiały oraz szkolenia dla efektywnej pracy i rozwoju Twoich kompetencji, proponujemy system podwyższenia stawki początkowej. Wymiar pracy dostosowany jest do Twojej dyspozycyjności. Brzmi interesująco? A w rzeczywistości wygląda jeszcze ciekawiej! 📩 📩 📩 Osoby zainteresowane prosimy o przesłanie CV z aktualną klauzulą RODO oraz kilku zdań o sobie na adres poland.robocode@gmail.com. Więcej o nas przeczytać można na stronie robocode.pl. Czekamy na Ciebie w naszym super zespole! 😊
  12. W tej części kursu wracamy do tematu tablic, ale tym razem na dobre 🥳 Wiemy już jak mniej więcej wygląda tablica (jednowymiarowa tablica statyczna) oraz jak wygląda pamięć operacyjna. Tym razem przypatrzymy się bliżej tworzeniu i inicjalizacji elementów tablicy oraz poruszaniu się w pamięci. Spis treści #1 – wstęp i praktyka #2 – organizacja pamięci #3 – tablice w pamięci (czytasz ten artykuł) #4 – tablica jest wskaźnikiem #5 – znaki, cstring #6 – argumenty funkcji #7 – przykład #8 – tablice wielowymiarowe #9 – tablice dynamiczne #10 – zakończenie Dlaczego statyczna? Jak już wiemy istnieją dane statyczne – takie które zostały "powołane do życia" na etapie kompilacji i mają stałe miejsce w pamięci. Można tu wyróżnić zmienne globalne i zmienne z dopiskiem (słowem kluczowym/specyfikatorem) static. Jest to tylko informacja dla kompilatora ile bajtów danych powinien (zarezerwować) zaalokować w pamięci. Statycznie nie znaczy stałe – zmienne można nadpisywać (nie jest to specyfikator const czy constexpr). Są to bardzo ważne informacje, ponieważ jak wiemy tablica służy do przechowywania danych tego samego typu, ale mogą mieć różną liczbie elementów. Skoro wiec kompilator życzy sobie mieć podany rozmiar obszaru pamięci jaki ma przydzielić określonym danym statycznym, to dlatego musimy w jakiś sposób podać informację ile ma być tych danych. Możemy tego dokonać na kilka sposobów. Tworzenie tablic statycznych W jednym z przykładów pokazałem zapis: int lockers[5]; Wiemy, że wpisywanie magicznych liczb nie jest dobrą praktyką, co więc powinno się tu znaleźć? Coś co znane jest na etapie kompilacji. Można tu więc podać wyrażenie, które utworzy dla nas preprocesor (działający przed kompilacją na kodzie źródłowy). Dyrektywą #define utworzy alias na ciąg znaków (tzw. tokenów) LOCKERS_COUNT. Wystąpienia aliasu zostaną przetłumaczone na token 5 i wstawione w kod źródłowy, który zostanie przekazany do kompilacji. #define LOCKERS_COUNT 5 int lockers[LOCKERS_COUNT]; Takie podejście przez kompilator jest widziane tak samo jak używanie magicznych liczb, ale na etapie pisania kodu poprawia czytelność i jest wspierane prze środowiska programistyczne (IDE). Użycie dyrektywy zamiast zmiennej pozwala uniknąć alokacji pamięci operacyjnej. Tak przygotowany alias możemy używać w różnych miejscach kodu, np. w pętli: for(int i = 0; i < LOCKERS_COUNT; i++) { Serial.print(F("Locker ")); Serial.print(i); if(lockers[i]) Serial.println(F(" is open.")); else Serial.println(F(" is closed.")); } Efekt działania preprocesora widoczny jest w pliku wynikowym w katalogu preproc, który znajdziemy w tymczasowym katalogu roboczym Ardunio IDE – jego ścieżkę znajdziemy przeglądając log kompilacji. Innym sposobem zadeklarowania tablicy jest użycie zmiennej wewnątrz nawiasów kwadratowych. Wymaga to tylko zapewnienia kompilatora, że to co tam wpisujemy jest stałe. Można tego dokonać dopisując przed typem danych zmiennej słowo kluczowe... const? Niekoniecznie... dlatego, że specyfikator const nie daje gwarancji, że w czasie kompilacji wartość będzie stała. Jak to? Prześledźmy przykład: void setup() { const int sensor_value = digitalRead(0); sensor_value = LOW; } Pierwsza linia jest w porządku – wartość raz ustalona, nie ulegnie zmianie, acz zmienna zapisana na stosie (adres 0x8F6). Druga linia spowoduje błąd kompilacji, bo zgodnie z prawdą raz ustalonej zmiennej const nie możemy zmienić. A czy da się użyć tej zmiennej do stworzenia tablicy statycznej? Da się: void setup() { const int sensor_value = digitalRead(0); int suspicious_tab[sensor_value]; } Nie jest to jednak poprawne, gdyż patrząc na ten kod nie da się stwierdzić jaki rozmiar ma tablica na etapie kompilacji. To dlaczego kompilacja przeszła? Bo aż do wersji C++11 był to dopuszczony sposób o ile był on używany z głową (podobnie jak nikt nie zabroni sięgnąć do pamięci poza ustalonym obszarem tablicy). W C++11 doszedł nowy specyfikator, który tworzy bardziej stałe... stałe, jest nim constexpr. Tego słowa kluczowego niestety nie znajdziemy wśród typowej składni Arduino IDE (znajdziemy np. w Visual Studio Code), ale działa i nie da się go oszukać, bo wartość constexpr musi być znana na etapie kompilacji: void setup() { constexpr int really_constant_lenght = 5; int tab[really_constant_lenght]; } Takie coś uda się na pewno. Pamiętajmy, że choć constexpr jest stałe to nie jest niezniszczalne - ten egzemplarz został zapisany na stosie więc po opuszczeniu funkcji setup() zostanie usunięty. Inicjalizacja tablic statycznych Podobnie jak zwykłe zmienne, tablice też podlegają inicjalizacji wartościami początkowymi co pobieżnie sprawdziliśmy w pierwszej części kursu. Jeżeli tablica jest widoczna globalnie lub jest statyczna (tak zgadza się statyczna tablica statyczna) to zostanie zapisana w bloku pamięci statycznej. To czy dane tablicy trafią do sekcji .data czy .bss zależy od tego czy zostanie przez nas zainicjowana. Zasada jest prosta: Jeżeli nie zainicjowaliśmy żadnego elementu globalnej/statycznej tablicy to zostanie zarezerwowany blok pamięci w .bss. Przykład: int lockers[LOCKERS_COUNT]; Jak wiemy do inicjalizacji możemy wykorzystać nawiasy klamrowe. Co ciekawe, nie musimy w takim przypadku inicjalizować wszystkich wartości, bo jeżeli mamy tablicę globalną/statyczną to pozostałe elementy (zgodnie z zadanym rozmiarem tablicy) otrzymają domyślne 0: int lockers[LOCKERS_COUNT] = {1, 0, 1}; // wartości tablicy: {1, 0, 1, 0, 0} Taka tablica trafi (zostanie zaalokowany obszar) w sekcji .data. W ramach ciekawostki – znak równości można pominąć (podobnie co z przypadku pojedynczej zmiennej): int lockers[LOCKERS_COUNT] {1, 0, 1}; // wartości tablicy: {1, 0, 1, 0, 0} A co z tablicami lokalnymi? Zasada jest ta sama co w przypadku zmiennych lokalnych. Tak naprawdę tablica to taka bardziej złożona zmienna obejmująca dużo więcej miejsca i posiadająca pewną specyficzną cechę - możliwość indeksowania. Tablica w pamięci Dobrze, a jak wygląda pamięć zajęta przez tablicę? Na wstępie do kursu wspomniałem, że wrócimy do podanej definicji tablicy, gdyż nie omówiłem na czym polega ciągłość bloku pamięci który zajmuje. Jako, że dane w tablicy są usystematyzowane i możliwe jest sprawne przechodzenie pomiędzy jej elementami, to też miejsce w pamięci jest zajęte w usystematyzowany sposób. Każda z wartości ma swój adres, który możemy uzyskać w taki sam sposób jak w przypadku zwykłej zmiennej: int *address_locker_3 = &lockers[3]; Zapis po lewej stronie już widzieliśmy. Wyrażenie po prawej stronie też jest znane, tylko że w miejsce nazwy zwykłej zmiennej podajemy wybrany element tablicy (tu 3). Wiedząc to spróbujmy wypisać wszystkie adresy i wartości tablicy z przykładu: int lockers[LOCKERS_COUNT] = {1, 0, 1}; void setup() { Serial.begin(9600); for (int i = 0; i < LOCKERS_COUNT; i++) { Serial.print(F("0x")); Serial.print((int)&lockers[i], HEX); Serial.print(F(" locker: ")); Serial.println(lockers[i]); } } Otrzymamy takie wyniki: 0x102 locker: 1 0x104 locker: 0 0x106 locker: 1 0x108 locker: 0 0x10A locker: 0 Jak widać elementy tablicy zaczynają się od adresu 0x102 i są zwiększane co 2 B (tyle ile zajmuje typ danych int). Dlatego możliwa jest bezpieczna iteracja. W bloku pamięci tablicy nie ma żadnych przerw. A jak wygląda sytuacja, w której wyjdziemy poza tablicę? Co się wtedy stanie? Dla przedstawienia efektu po tablicy utwórzmy zmienną w sekcji .data: int lockers[LOCKERS_COUNT] = {1, 0, 1}; int notimportant = (1<<15) - 1; // maksymalna wartość int (ze znakiem) void setup() { Serial.begin(9600); for (int i = 0; i < LOCKERS_COUNT + 1; i++) { Serial.print(F("0x")); Serial.print((int)&lockers[i], HEX); Serial.print(F(" locker: ")); Serial.println(lockers[i]); } } Zgodnie z oczekiwaniem kod się skompilował, bezbłędnie uruchomił i odczytaliśmy wartość sąsiedniego bloku pamięci (akurat udało się pobrać wartość zmiennej): 0x102 locker: 1 0x104 locker: 0 0x106 locker: 1 0x108 locker: 0 0x10A locker: 0 0x10C locker: 32767 Widzimy zatem jak niebezpieczne jest złe indeksowanie. Dobrze, że tylko odczytywaliśmy dane i udało się trafić na ten sam typ danych, gorzej gdybyśmy chcieli coś zapisać do takiej "tablicy". Dygresja o skakaniu Z powyższego przykładu, choć pokazującego błędne zachowanie można wyciągnąć wniosek – możliwe jest skakanie pomiędzy obszarami pamięci – udało się wyskoczyć z tablicy i przejść do innej zmiennej. Czy gdybyśmy zamiast tablicy mieli kilka zmiennych, czy możliwe byłoby przechodzenie pomiędzy nimi? Załóżmy że zmienne te są zaalokowane w kolejności. Bierzemy adres pierwszej z nich: int variable_1 = 6; int variable_2 = 123; int variable_3 = -555; int *var_1_address = &variable_1; Będzie to adres 0x100, kolejny będzie o 2 B większy więc będzie to 0x102, kolejny 0x104. Wydaje się, że wystarczy dodać do pierwszego adresu 2 i gotowe. Niekoniecznie... int variable_1 = 6; int variable_2 = 123; int variable_3 = -555; int *var_1_address = &variable_1; int *var_2_address = var_1_address + 2; void setup() { Serial.begin(9600); Serial.print(F("0x")); Serial.print((int)&variable_1, HEX); Serial.print(F(", variable_1: ")); Serial.println(variable_1); Serial.print(F("0x")); Serial.print((int)&variable_2, HEX); Serial.print(F(", variable_2: ")); Serial.println(variable_2); Serial.print(F("0x")); Serial.print((int)&variable_3, HEX); Serial.print(F(", variable_3: ")); Serial.println(variable_3); Serial.print(F("0x")); Serial.print((int)var_1_address, HEX); Serial.print(F(", var_1_address: ")); Serial.println(*var_1_address); Serial.print(F("0x")); Serial.print((int)var_2_address, HEX); Serial.print(F(", var_2_address: ")); Serial.println(*var_2_address); } void loop() { } Zwróćmy uwagę na ostatnią linię w wyniku ze zmienną var_2_address: 0x100, variable_1: 6 0x102, variable_2: 123 0x104, variable_3: -555 0x100, var_1_address: 6 0x104, var_2_address: -555 Nie tak miało być, ale widać że dodawanie działa! W poprzednim rozdziale wspomniałem, że wskaźnik niesie informację o wielkości obszaru pamięci, więc to że int zajmuje 2 B jest zachowane. Dodając coś do adresu zwiększamy go nie o liczbę bajtów tylko o liczbę związaną z obszarem zajmowanym przez ten typ danych. Dlatego wskaźnik zazwyczaj jest skojarzony z konkretnym typem danych. Przykładowo, utwórzmy zmienną int (2 B) i dalej 2 zmienne char (2 x 1 B) i odczytajmy wartość używając wskaźnika na int. Wartości wypiszmy w systemie szesnastkowym: int variable_1 = 6; char variable_2 = 33; // 0x21 char variable_3 = 76; // 0x4C int *var_1_address = &variable_1; int *var_2_address = var_1_address + 1; void setup() { Serial.begin(9600); Serial.print(F("0x")); Serial.print((int)&variable_1, HEX); Serial.print(F(", variable_1: ")); Serial.println(variable_1, HEX); Serial.print(F("0x")); Serial.print((int)&variable_2, HEX); Serial.print(F(", variable_2: ")); Serial.println(variable_2, HEX); Serial.print(F("0x")); Serial.print((int)&variable_3, HEX); Serial.print(F(", variable_3: ")); Serial.println(variable_3, HEX); Serial.print(F("0x")); Serial.print((int)var_1_address, HEX); Serial.print(F(", var_1_address: ")); Serial.println(*var_1_address, HEX); Serial.print(F("0x")); Serial.print((int)var_2_address, HEX); Serial.print(F(", var_2_address: ")); Serial.println(*var_2_address, HEX); } Co prawda odczytaliśmy kolejny adres 0x102 (ostatnia linia) ale nie taka wartość miała być: 0x100, variable_1: 6 0x102, variable_2: 21 0x103, variable_3: 4C 0x100, var_1_address: 6 0x102, var_2_address: 4C21 Jak widać sąsiednie bajty zostały złączone tak by obszar pamięci pasował do typu danych int. Sytuację można poprawić zmieniając typ danych wskaźnika na ten skojarzony z typem char, oraz rzutując wynik operacji dodawania: int *var_1_address = &variable_1; char *var_2_address = (char*)(var_1_address + 1); Jak można zauważyć przesunięcie adresu jest zgodne z typem danych int, a odczytanie wartości odbywa się zgodnie z char: 0x100, variable_1: 6 0x102, variable_2: 21 0x103, variable_3: 4C 0x100, var_1_address: 6 0x102, var_2_address: 21 "Ciekawe... a do czego mi to potrzebne, skoro tak nie powinno się poruszać pomiędzy zmiennymi?" – taka myśl może się zrodzić, bo wielokrotnie wspominałem, że takie skanie jest dozwolone tylko w obrębie tablic i właśnie tam wykorzystamy podobny mechanizm. Znajdując się w ostatniej scenie typowego cliffhangera zapowiem, że w kolejnej części przybliżę temat tablic, w których nie będziemy posługiwać się nawiasami kwadratowymi do poruszania siępo elementach. W tej części przybliżyłem różne sposoby tworzenia tablic statycznych oraz alokacji w pamięci. Oczywiście tablice statyczne to nie jedyny rodzaj tablic wiec w przyszłości wrócimy do tego tematu.
  13. Pisząc drobne programiki na AVR w C nie był możliwy taki konstrukt: int w = 10; // przykład, w może oznaczać rozmiar ciągu danych np: z uart char str[w]; Tymczasem działając teraz z programem pod esp8266 tak z ciekawości napisałem: int packetSize = Udp.parsePacket(); char packetBuffer[packetSize]; Pytanie teraz do ekspertów: czy powyższy zapis jest prawidłowy? to się kompiluje i działa ale czy nie powinienem zastosować jakichś funkcji do dynamicznej alokacji danych?
  14. O ile wydrukowanie stringu z flasch jest dla mnie oczywiste proste i zrozumiałe Serial.print(F("test")); to nie bardzo wiem jak tego makra(PGMP) użyć w funkcjach z końcówką _P np jak zapisać wersję tego: strcmp("Audyt N. 7 ",s) Tak aby string "Audyt N. 7 " był pobierany bezpośrednio z flasch? np: gdy próbuję zapisać (to jest cały warunek) if((lineMem == 0) && (0 == strcmp_P("Audyt N. 7 ",s))) Kompilacja przebiega prawidłowo jednak zajętość RAM nie zmienia się, a chyba powinna być pomniejszona o ilość znaków napisu Audyt..... ?
  15. Dzień dobry, napisałem kod programu który powinien wczytać wiadomość z pliku tekstowego, następnie ją zaszyfrować i wrzucić do następnego pliku. Niestety tak się nie dzieje : P Mógłbym prosić o pomoc kogoś o pomoc? Edit: Udało mi się samemu rozwiązać problem 🙂 Rozwiązanie (może komuś się przyda 🙂 ) : #include <iostream> #include <fstream> using namespace std; string Cezar(string s, int klucz) { ifstream wejscie("tekst_jawny.txt"); wejscie>>s; cout << "Klucz: "; cin>>klucz; string szyfrogram=""; int kod; for(int i=0;i<s.size();i++) if (toupper(s[i])>='A' && toupper(s[i])<='Z') { kod=s[i]+klucz; if((s[i]<='Z' && kod>'Z') || (s[i]>='a' && kod>'z')) kod=kod-26; szyfrogram=szyfrogram+char(kod); } else szyfrogram=szyfrogram+s[i]; return szyfrogram; } int main() { int klucz; ifstream wejscie("tekst_jawny.txt"); ofstream wyjscie ("szyfrogram.txt"); string s; while (!wejscie.eof()) { getline(wejscie,s); wyjscie<<Cezar(s,klucz)<<endl; } wejscie.close(); wyjscie.close(); cout<<"Plik szyfrogram.txt zostal utworzony"; return 0; }
  16. Ten artykuł jest częścią serii "Arduino dla początkujących: instrukcje sterujące w C/C++" #1 - if, bloki #2 - while i coś jeszcze #3 - for, break, switch W poprzednich częściach artykułu poznaliśmy dwie podstawowe instrukcje sterujące: if i while. Teraz kolej na coś bardziej rozbudowanego. Przyjrzyjmy się jak wygląda Pętla z efektami, czyli instrukcja "for" Ten artykuł bierze udział w naszym konkursie! 🔥 Na zwycięzców czekają karty podarunkowe Allegro, m.in.: 2000 zł, 1000 zł i 500 zł. Potrafisz napisać podobny poradnik? Opublikuj go na forum i zgłoś się do konkursu! Czekamy na ciekawe teksty związane z elektroniką i programowaniem. Sprawdź szczegóły » Zapis tej instrukcji jest następujący: for (START; WARUNEK; KROK) INSTRUKCJA; Wykonuje się ona w sposób następujący: Najpierw wykonywana jest instrukcja określona jako START. Następnie sprawdzany jest WARUNEK. Jeśli nie jest spełniony następuje wyjście z pętli. Jeśli jest - wykonywane są kolejno INSTRUKCJA i KROK, a następnie następuje powrót do sprawdzenia warunku. Wszystkie trzy wyrażenia (START, WARUNEK i KROK) mogą być pominięte, przy czym pominięcie WARUNEK jest równoznaczne z "prawda". Spróbujmy rozpisać to za pomocą goto: START; etykieta: if (WARUNEK) { INSTRUKCJA; KROK; goto etykieta; } Możemy to zobrazować również za pomocą diagramu: Możemy sobie napisać prosty program, pokazujący działanie instrukcji for: #define BUTTON_PIN 2 void setup() { Serial.begin(9600); pinMode(BUTTON_PIN, INPUT_PULLUP); // ustawiamy pin jako wejście z podciągnięciem do Vcc } void loop() { if (digitalRead(BUTTON_PIN) == LOW) { int i; for ( i=0; i<10; i += 1) { Serial.println(i); } } } Jako ćwiczenie możemy przewidzieć, jak będzie wyglądać działanie programu a potem, po uruchomieniu sprawdzić, czy zachowuje się zgodnie z naszymi oczekiwaniami. Obie instrukcje (while i for) mają pewną "zdegenerowaną" formę, czyli w skrócie "kręć się w kółko". Będą to odpowiednio: while(1) INSTRUKCJA; // nieskończona pętla while for(;;) INSTRUKCJA; // nieskończona pętla for Jak jednak wyjść z takiej nieskończonej pętli? Oczywiście, zawsze się znajdzie Wyjście awaryjne, czyli instrukcja "break" Wykonanie instrukcji break powoduje natychmiastowe przerwanie pętli i wznowienie wykonania od następnej instrukcji za pętlą. Czyli jeśli w programie mamy konstrukcję: while (WARUNEK) { INSTRUKCJE; if (przerywamy) break; INSTRUKCJE; } to możemy ją sobie wyobrazić jako: etykieta: if (WARUNEK) { INSTRUKCJE; if (przerywamy) goto etykieta2; INSTRUKCJE; goto etykieta; } } etykieta2: Za pomocą instrukcji break możemy oczywiście przerwać nie tylko nieskończone pętle ale również takie, które mają swój warunek kończący. Instrukcja ta może oczywiście zawierać się w zagnieżdżonym bloku - jej wykonanie spowoduje wyjście z bloku i zniszczenie należących do niego zmiennych, tak jakby było to normalne dojście programu do końca bloku. Tak więc pętle mamy już opanowane - pozostała nam jeszcze jedna instrukcja, a mianowicie if na sterydach, czyli instrukcja "switch" Za pomocą tej instrukcji możemy wybrać jedną z kilku możliwości, nie jesteśmy ograniczeni do dwóch. Przykładowy zapis będzie wyglądać w ten sposób: switch(WYRAŻENIE) { case STAŁA1: INSTRUKCJE1; case STAŁA2: INSTRUKCJE2; case STAŁA3: INSTRUKCJE3; default: INSTRUKCJE4; } Jak widać, nie jesteśmy tu ograniczeni do pojedynczej instrukcji jak poprzednio; INSTRUKCJE to po prostu ciąg dowolnej ilości instrukcji, z zerem włącznie, np. tak: switch(x) { case 1: case 2: // robimy coś case 3: // robimy coś innego default: // robimy coś jeszcze innego } Oczywiście i tu mamy pewne ograniczenia: WYRAŻENIE powinno zwracać wartość typu int, a argumentem case mpożę być wyłącznie stała tego typu. Tak więc konstrukcje typu: int n,x; n= cośtam(); x=cośtam(); switch(x) { case n: albo (widywana w próbach rozwiązań zadań z kursu): String s; s=cośtam(); switch(s) { case "zielona": będą nieprawidłowe; pierwsza ponieważ argumentem case nie jest stała, drugie ponieważ zarówno wyrażenie, jak i stała napisowa użyta w case nie jest typu int. I tu bardzo ważna uwaga. Dojście do kolejnej etykiety case nie przerywa toku programu; zostają wykonane następne instrukcje. Najlepiej wyjaśni to rozpisanie za pomocą goto: R=WYRAŻENIE; // gdzie R to wewnętrzna, niejawna zmienna instrukcji switch if (R == STAŁA1) goto etykieta1; // case STAŁA1 if (R == STAŁA2) goto etykieta2; // case STAŁA2 if (R == STAŁA3) goto etykieta3; // case STAŁA3 goto etykieta4; //default etykieta1: INSTRUKCJE1; etykieta2: INSTRUKCJE2; etykieta3: INSTRUKCJE3; etykieta4: INSTRUKCJE4; lub za pomocą diagramu: Klauzula default powinna zawsze wystąpić w instrukcji switch, jednak jej brak nie jest błędem. W takiej sytuacji kompilator uzna, że domyślnym zachowaniem w przypadku, gdy wartość wyrażenia nie jest równa żadnej z wartości w klauzulach case jest "nic nie rób" - tak jakby przed zamykającym nawiasem dopisywał sobie dodatkową, brakującą etykietę default. Zostało nam jedno: dojście do kolejnego case nie powoduje przerwania instrukcji switch, ale w większości przypadków jest nam to potrzebne. Tak jak w instrukcji if mamy dwie niezależne gałęzie, tak tu chcielibyśmy mieć ich więcej, równie niezależnych. Do tego celu służy nam znana już instrukcja break. Jej wykonanie powoduje natychmiastowe zakończenie wykonania instrukcji switch. Przykładowy kod może wyglądać tak: switch(WYRAŻENIE) { case STAŁA1: INSTRUKCJE1; break; case STAŁA2: INSTRUKCJE2; break; case STAŁA3: INSTRUKCJE3; break; default: INSTRUKCJE4; } W ten sposób zależnie od wartości wyrażenia zostanie wykonany jeden, konkretny ciąg instrukcji. Co więcej, kompilator (jeśli włączymy wszystkie ostrzeżenia) będzie spodziewał się, że instrukcja break jest w danym miejscu (czyli przed case/default) potrzebna i jej brak uzna za nasze przeoczenie. Czyli fragment typu: case 1: x = x+1; case 2: y = y-1; uzna za co najmniej podejrzany. Jak mu wyjaśnić, że o to nam chodziło i ma się nie wtrącać? Do tego służy specjalna postać komentarza. Jeśli przekształcimy nasz kod w sposób następujący: case 1: x = x+1; // pass thru case 2: y = y+1; kompilator uzna, że komentarz "pass thru" oznacza, że tak miało być i tu nie trzeba żadnego break. I to już wszystko - możemy zabrać się do pisania programów. I na koniec przypomnienie: co prawda instrukcja goto występuje w językach C i C++, ale umówmy się: nie będziemy jej używać w rzeczywistych programach. Ale... jeśli ktoś zetknął się z sytuacją, której nie można rozwiązać tylko za pomocą omawianych instrukcji (bez goto) albo przynajmniej jest to utrudnione - niech da znać w komentarzach!
  17. Ten artykuł jest częścią serii "Arduino dla początkujących: instrukcje sterujące w C/C++" #1 - if, bloki #2 - while i coś jeszcze #3 - for, break, switch Czytając posty zamieszczane przez początkujących użytkowników można zauważyc pewną regułę: otóż większość ma problem ze zrozumieniem, jak działają instrukcje sterujące. O ile z prostą instrukcją if prawie każdy radzi sobie bez problemu, o tyle instrukcje pętli (while, for) czy wyboru (switch) nader często stosowane są nieprawidłowo. Spróbujmy temu zaradzić. Podstawa wszelkich podstaw, czyli instrukcja "if" Na początek zajmijmy się instrukcja if. Jej zadanie jest proste: jakaś inna instrukcja zostanie wykonana tylko wtedy, jeśli jakiś warunek jest spełniony. Zapiszmy to w taki sposób: if (WARUNEK) INSTRUKCJA; Warunkiem może być tu dowolne wyrażenie zwracające wartość. Możemy to pokazać na przykładzie. Załóżmy, że do naszego Arduino do pinu 2 podłączymy przycisk: Skorzystamy tu z wbudowanej diody LED, aby nie dodawać dodatkowych elementów. Ten artykuł bierze udział w naszym konkursie! 🔥 Na zwycięzców czekają karty podarunkowe Allegro, m.in.: 2000 zł, 1000 zł i 500 zł. Potrafisz napisać podobny poradnik? Opublikuj go na forum i zgłoś się do konkursu! Czekamy na ciekawe teksty związane z elektroniką i programowaniem. Sprawdź szczegóły » No i zadanie pierwsze - niech dioda zapala się gdy naciśniemy przycisk. Nasz program będzie wyglądać tak: #define BUTTON_PIN 2 #define LED_PIN 13 void setup() { pinMode(BUTTON_PIN, INPUT_PULLUP); // ustawiamy pin jako wejście z podciągnięciem do Vcc pinMode(LED_PIN, OUTPUT); //ustawiamy pin jako wyjście digitalWrite(LED_PIN, LOW); // i ustawiamy stan niski na pinie } void loop() { if (digitalRead(BUTTON_PIN) == LOW) digitalWrite(LED_PIN, HIGH); // zapalamy ledę po wciśnięciu } Po uruchomieniu widzimy, że po naciśnięciu przycisku dioda się zapala. Zaraz... a czemu po puszczeniu nie gaśnie? Aha, bo w programie nie ma przecież nic o gaszeniu, prawda? Trzeba to natychmiast naprawić! void loop() { if (digitalRead(BUTTON_PIN) == LOW) digitalWrite(LED_PIN, HIGH); // zapalamy ledę po wciśnięciu if (digitalRead(BUTTON_PIN) == HIGH) digitalWrite(LED_PIN, LOW); // gasimy ledę po puszczeniu } No, teraz działa. Ale czy to naprawdę ma wyglądać? Dwa razy wywołujemy funcję digitalRead, nie da się prościej? Oczywiście, że się da! Po prostu instrukcja if ma jeszcze drugą postać: if (WARUNEK) INSTRUKCJA1; else INSTRUKCJA2; Jeśli użyjemy tej formy, INSTRUKCJA1 zostanie wykonana jeśli warunek jest spełniony, w przeciwnym razie zostanie wykonana INSTRUKCJA2. Poprawmy więc nasz program (przy okazji trochę go uczytelniając): void loop() { if (digitalRead(BUTTON_PIN) == LOW) digitalWrite(LED_PIN, HIGH); // zapalamy ledę po wciśnięciu else digitalWrite(LED_PIN, LOW); // gasimy ledę po puszczeniu } Jakoś ładniej, prawda? I działa tak samo! Ponieważ w dalszej części będziemy posługiwać się diagramami, zobaczmy jak na takim diagramie będzie wyglądać instrukcja if: Zajmijmy się teraz warunkiem. W językach C i C++ warunkiem - jak wspomniano - może być dowolne wyrażenie zwracające wartość. Ta wartość zostaje przekształcona na typ int. Warunek jest spełniony, jeśli po owym przekształceniu wartość jest różna od zera. Tak więc wynikiem porównania (jak w naszym programie) jest wartość logiczna "prawda" lub "fałsz". Po przekształceniu na typ int "fałsz" jest zerem, natpomiast "prawda" to jakaś wartość różna od zera (nie interesuje nas na razie jaka konkretnie, ważne że to nie zero). Możemy również sprawdzić, że Arduinowe LOW to zero, a HIGH to jeden. Tak więc możemy jeszcze bardziej uprościć nasz program: void loop() { if (digitalRead(BUTTON_PIN)) // przycisk puszczony digitalWrite(LED_PIN, LOW); // a więc gasimy ledę else digitalWrite(LED_PIN, HIGH); // a jak wciśnięty to zapalamy } Zapis z porównaniem jest jednak - przynajmniej dla początkujących - bardziej czytelny, i takim będziemy się dalej posługiwać. Spróbujemy teraz czegoś trudniejszego. Ponieważ instrukcja if zawiera inną instrukcję, nic nie stoi na przeszkodzie aby tą inną instrukcją był również if. W ten sposób możemy sobie zagnieżdżać warunki i tworzyć całkiem skomplikowane programy. Podłączmy teraz drugi przycisk, przykładowo do pinu 3. Niech ten przycisk będzie przyciskiem zezwalającym na zapalanie ledy: czyli będzie ona reagować na wciśnięcie pierwszego przycisku tylko wtedy, gdy drugi jest wciśnięty; puszczenie pierwszego przycisku nie powinno wywołać żadnej reakcji, natomiast puszczenie drugiego powoduje zgaśnięcie ledy. Spróbujmy więc napisać program: #define BUTTON_PIN 2 #define ENABLE_PIN 3 #define LED_PIN 13 void setup() { pinMode(BUTTON_PIN, INPUT_PULLUP); // ustawiamy pin jako wejście z podciągnięciem do Vcc pinMode(ENABLE_PIN, INPUT_PULLUP); // drugi pin tak samo pinMode(LED_PIN, OUTPUT); //ustawiamy pin jako wyjście digitalWrite(LED_PIN, LOW); // i ustawiamy stan niski na pinie } void loop() { if (digitalRead(ENABLE_PIN) == LOW) // jeśli przycisk ENABLE jest wciśnięty if (digitalRead(BUTTON_PIN) == LOW) // teraz możemy zapalić ledę digitalWrite(LED_PIN, HIGH); else // przycisk ENABLE jest puszczony digitalWrite(LED_PIN, LOW); } Uruchamiamy nasz program, sprawdzamy i... oj, coś tu jest nie tak! Ano jest. Kompilator po prostu przypisał "else" do wewnętrznego ifa, i dlatego leda gaśnie od razu po puszczeniu pierwszego przycisku jeśli drugi pozostał wciśnięty. Trochę nie o to nam chodziło... jak to naprawić? Spróbujmy: E pluribus unum, czyli blok instrukcji Otóż w C/C++ istnieje właśnie pojęcie bloku. Taki blok może w większości przypadków zastąpić pojedynczą instrukcję - przede wszystkim właśnie w instrukcjach sterujących. Zapisujemy go w ten sposób: { // początek bloku instrukcja pierwsza; instrukcja druga; instrukcja trzecia; // i kolejne } // koniec bloku Spróbujmy więc poprawić nasz program: void loop() { if (digitalRead(ENABLE_PIN) == LOW) // jeśli przycisk ENABLE jest wciśnięty { // rozpoczynamy blok if (digitalRead(BUTTON_PIN) == LOW) // teraz możemy zapalić ledę digitalWrite(LED_PIN, HIGH); } // kończymy blok else // przycisk ENABLE jest puszczony digitalWrite(LED_PIN, LOW); } No, teraz działa! Blok instrukcji ma kilka dodatkowych cech. Najważniejszą z nich jest ta, że może zawierać zmienne lokalne, które będą widoczne tylko wewnątrz bloku i będą niszczone po wyjściu z niego. Ta cecha jest bardzo ważna szczególnie w przypadku małych procesorków (jak Arduinowa ATmega). Wyobraźmy sobie taką sytuację: void loop() { int a[500]; char b[1000]; // tu fragment programu operujący na tablicy a // a tu fragment programu operujący na tablicy b } Niestety - próba wykonania takiego programu skończy się smętnie: Arduino nie ma tyle pamięci, aby pomieścić obie tablice. Możemy jednak temu zaradzić właśnie używając bloków: void loop() { { // rozpoczynamy blok int a[500]; // tu fragment operujący na tablicy a } // zamykamy blok niszcząc przy okazji tablicę a { // rozpoczynamy drugi blok int b[1000]; // tu fragment operujący na tablicy b } // zamykamy blok niszcząc przy okazji tablicę b } Tak więc w dalszej części artykułu wszędzie gdzie pojawi się INSTRUKCJA (czy też INSTRUKCJA1, INSTRUKCJA2 itd) będziemy mogli wstawić blok instrukcji. Tyle na razie, w następnej części spróbujemy rozpracować pętlę oraz poznamy pewną nieznaną instrukcję o której się nie wspomina.
  18. Ten artykuł jest częścią serii "Arduino dla początkujących: instrukcje sterujące w C/C++" #1 - if, bloki #2 - while i coś jeszcze #3 - for, break, switch Po przebrnięciu przez pierwszą część możemy założyć, że instrukcja if nie sprawi nam już trudności. Zanim jednak przejdziemy dalej, musimy uświadomić sobie, że istnieje pewna instrukcja która z reguły nie jest omawiana w kursach i tutorialach, i stosowanie jej poza bardzo wyjątkowymi przypadkami nie jest wskazane. Instrukcja ta zaburza bowiem naturalne wykonanie programu, sprawia trudności przy czytaniu a w niektórych przypadkach albo jej wykonanie w ogóle nie będzie możliwe (kompilator od razu stwierdzi że "tak nie można"), albo może spowodować różne dziwne i niepożądane skutki uboczne. Jednak kompilator przekładając nasz program na kod maszynowy musi z niej korzystać. Czyli przed nami Podstawa ukryta, czyli "goto" W dawnych czasach, kiedy języki programowania były bardzo proste i nie zawierały w ogóle żadnych instrukcji sterujących poza ifem, stosowanie goto było po prostu koniecznością. Dobrym przykładem jest tu język BASIC, a szczególnie jego odmiana na ośmiobitowe Commodore czy Atari: jedyną możliwością zrealizowania jakichś bardziej skomplikowanych algorytmów była właśnie instrukcja skoku połączona z instrukcją if. Jednak wraz z rozwojem języków instrukcja ta odchodziła w cień i w końcu doszło do tego, że niektóre nowoczesne języki (np. Python) w ogóle takiej instrukcji ani żadnego odpowiednika nie zawierają. W C/C++ jednak pozostała, i do celów demonstracji zachowania instrukcji sterujących możemy jej użyć. Ten artykuł bierze udział w naszym konkursie! 🔥 Na zwycięzców czekają karty podarunkowe Allegro, m.in.: 2000 zł, 1000 zł i 500 zł. Potrafisz napisać podobny poradnik? Opublikuj go na forum i zgłoś się do konkursu! Czekamy na ciekawe teksty związane z elektroniką i programowaniem. Sprawdź szczegóły » Zapis tej instrukcji jest prosty: goto etykieta; // kontynuujemy wykonanie programu od innego miejsca // tu jakiś fragment programu etykieta: // program po wykonaniu goto "przeskoczy w to miejsce" // dalsza część programu Wyobraźmy teraz sobie, że instrukcja if nie może zawierać innej instrukcji niż goto. Spróbujmy rozpisać sobie prostą instrukcję if zawierającą obie gałęzie: if (WARUNEK) goto etykieta1; // skaczemy do fragmentu wykonywanego po spełnieniu warunku // tu fragment wykonywany w else, czyli przy niespełnionym warunku goto etykieta2; // po wykonaniu musimy pominąć fragment programu etykieta1: // tu fragment programu wykonywany przy spełnionym warunku etykieta2: // i koniec instrukcji if Skomplikowane? Niestety, trochę. Ale bez zrozumienia tej konstrukcji nie możemy zrozumieć kolejnych, a taką będzie Najprostsza pętla, czyli instukcja "while" Instrukcja zapisywana jest jako: while (WARUNEK) INSTRUKCJA; Wykonuje się ona bardzo prosto: jeśli warunek jest spełniony, następuje wykonanie INSTRUKCJA i powrót do testowania warunku. Jeśli warunek nie jest wpełniony, wykonanie instrukcji kończy się. Diagram wykonania takiej instrukcji jest stosunkowo prosty: Aby lepiej zrozumieć działanie, rozpiszmy to sobie z użyciem instrukcji goto: etykieta: if (WARUNEK) { INSTRUKCJA; goto etykieta; } I tu uwaga: trzeba pamiętać o tym, że WARUNEK musi być zależny od czynników zewnętrznych, albo INSTRUKCJA w jakiś sposób musi wpływać na to, czy w następnym obrocie pętli warunek jest spełniony. Takim sztandarowym przykładem będzie tu odczytanie stanu pinu (np. wciśnięcia klawisza): while (digitalRead(BUTTON_PIN) == LOW) { // tu jakiś fragment wykonywany dopóki klawisz jest wciśnięty } Możemy spróbować napisać jakiś program, który będzie wykorzystywać tę instrukcję, przykładowo: #define BUTTON_PIN 2 #define LED_PIN 13 void setup() { pinMode(BUTTON_PIN, INPUT_PULLUP); // ustawiamy pin jako wejście z podciągnięciem do Vcc pinMode(LED_PIN, OUTPUT); //ustawiamy pin jako wyjście digitalWrite(LED_PIN, LOW); // i ustawiamy stan niski na pinie } void loop() { while (digitalRead(BUTTON_PIN) == LOW) { dopóki przycisk jest wciśnięty digitalWrite(LED_PIN, HIGH); // zapalamy ledę delay(250); // czekamy ćwierć sekundy digitalWrite(LED_PIN, LOW); // gasimy ledę delay(250); // czekamy ponownie } } Odmianą instrukcji while jest instrukcja do. Podstawową różnicą jest ta, że sprawdzenie warunku odbywa się po wykonaniu "wnętrza" instrukcji, czyli nasza INSTRUKCJA będzie wykonana co najmniej raz. Ze względu na podobieństwo nie będziemy się teraz bliżej nią interesować. Ale przy okazji możemy dowiedzieć się nowej rzeczy. Otóż - jak wspomniano na początku - WARUNEK musi być wyrażeniem zwracającym wartość. Trzeba wiedzieć, że instrukcja podstawienia w C/C++ również zwraca wartość - tę mianowicie, która została podstawiona. W wielu przypadkach może to uprościć program. Przykładowo - chcemy, aby jakiś fragment programu wykonywał się dopóki wynik odczytany z wejścia analogowego przekracza jakąś wartość, ale we wnętrzu pętli chcielibyśmy mieć dostęp do tego wyniku. Możemy to zapisać w ten sposób: void setup() { Serial.begin(9600); } void loop() { int x; while ((x = analogRead(A0)) > 500) { Serial.println(x); delay(500); } } Jeśli teraz podłączymy potencjometr do naszego Arduino, obracając jego ośką zobaczymy, że na monitorze będą wypisywane wyłącznie wartości większe od 500. Tyle w części drugiej, w następnej rozpracujemy działanie pętli for oraz instrukcji wyboru switch.
  19. Stali czytelnicy forbot.pl z pewnością wiedzą już czym jest oraz do czego przysłużyć może się framework Qt do C++. Jednym z nieodłącznych elementów tego frameworka jest moduł Qt Quick oparty na języku Qml. Pozwala on na budowanie multi-platformowych aplikacji w bardzo prosty i szybki sposób, a sam język jest bardzo naturalny. Ale co, jeżeli powiem Ci, że można pisać aplikacje w Qml jeszcze szybciej i jeszcze prościej? Przekonajmy się wspólnie czym jest Felgo SDK. Ten wpis brał udział konkursie na najlepszy artykuł o elektronice lub programowaniu. Sprawdź wyniki oraz listę wszystkich prac » Partnerem tej edycji konkursu (marzec 2020) był popularny producent obwodów drukowanych, firma PCBWay. Czym jest Felgo SDK? Felgo SDK rozszerza możliwości samego frameworka Qt dostarczając dodatkowe komponenty, których użycie ułatwia i przyspiesza programowanie z wykorzystaniem Qml przy czym projektami, na których SDK skupia się najbardziej są aplikacje oraz gry na platformy mobilne. Oczywiście nie oznacza to, że tych komponentów nie można zastosować w innych projektach opartych na Qml. Felgo świetnie sprawdza się w przypadku tworzenia oprogramowania desktopowego. Z uwagi na ogólną tendencję panującą w środowisku Qt, Felgo dostosowuje się do nowej rzeczywistości udostępniając swój silnik również dla tych, którzy chcą tworzyć efektywny interfejs użytkownika dla systemów wbudowanych. Kontynuując kwestię docelowych platform nie możemy pominąć kwestii przeglądarek i wykorzystania Qt for WebAssembly. Zgadza się, nie pomyliliśmy się tworząc ten artykuł. Już teraz możesz tworzyć aplikacje webowe z wykorzystaniem Qt! Co prawda projekt ten w dalszym ciągu nie działa do końca stabilnie, ale mam nadzieję, że wraz z Qt w wersji 6 wiele bolączek zostanie rozwiązanych. Niemniej wiele z tych problemów (np. z mechanizmem CORS przy wysyłaniu zapytań) zostanie rozwiązanych wraz z kolejną wersją Felgo, która powinna się pojawić w nadchodzących miesiącach. Oto lista platform, na które trafić może twoja aplikacja oparta na Felgo: Systemy wbudowane; Windows; Linux; macOS; Android; iOS; Web*; * Wsparcie dla projektów WebAssembly w Felgo powinno pojawić się w nadchodzących miesiącach. Korzyści programowania z Felgo Mamy już zarys tego czym jest Felgo i jakie projekty możemy w oparciu o to SDK rozwijać, ale pozostaje nam przyjrzeć się bliżej temu jakie konkretnie profity nam ono przyniesie. Felgo dla aplikacji mobilnych Jedną z głównych gałęzi Felgo jest wsparcie dla programistów tworzących rozwiązania w Qml na platformy mobilne. SDK dodaje do listy dostępnych w Qml typów własne komponenty, które albo rozszerzają możliwości tych już obecnych typów, albo dodają zupełnie nowe funkcjonalności. Spójrzmy na przykład na ten fragment kodu: import Felgo 3.0 import QtQuick 2.0 App { id: app NavigationStack { Page { title: "Forbot.pl" Column { anchors.centerIn: parent width: parent.width * 0.7 spacing: 40 AppTextField { width: parent.width inputMode: inputModeUsername } AppTextField { width: parent.width inputMode: inputModePassword } } } } } Efektem uruchomienia tego kodu będzie proste UI zawierające natywnie wyglądającą stronę z dwoma polami do wprowadzania tekstu. Typ AppTextField znacząco rozszerza dostępny w Qt Quick Controls typ TextField. Z jego wykorzystaniem możemy dodać kilka wizualnych detali bez konieczności tworzenia własnych, dodatkowych typów. Za tym z kolei idzie oszczędność przy pracy nad UI. Dodatkowo typ AppTextField posiada właściwość inputMode, której wartość wykorzystywana jest do automatycznego zmienienia zachowania pola. Na przykład wartość inputModePassword, zmieni maskowanie wprowadzonego tekstu i doda przycisk do zmiany tego maskowania. Z kolei wartości inputModeEmail i inputModeUrl mogą zostać wykorzystane, jeżeli chcemy walidować dane z wykorzystaniem właściwości acceptableInput. Tak prezentuje się aplikacja stworzona z wykorzystaniem powyższego kodu: Oczywiście wymieniony typ to tylko jeden z wielu udostępnionych komponentów. Pełną ich listę znajdziecie w dokumentacji. Natywna nawigacja Aplikacje dedykowane na iOS’a zwykły umożliwiać użytkownikowi nawigację po aplikacji z wykorzystaniem gestu przewijania w tył. Na Androidzie z kolei, aby wrócić do poprzedniej strony musisz kliknąć przycisk back. Na iOS menu wyświetlane jest u dołu ekranu, a na Androidzie z reguły dostępne jest po naciśnięciu na ikonę hamburgera. Co by nie mówić są różnice w implementacji nawigacji w aplikacjach mobilnych na różnych platformach i nie da się tego ukryć. Jeżeli chcielibyśmy zachować dedykowane danej platformie podejście musielibyśmy napisać dwa oddzielne i duże komponenty po jednym dla każdego z systemów. Felgo dostarcza własne typy ułatwiające implementację nawigacji. import Felgo 3.0 import QtQuick 2.0 App { Navigation { NavigationItem { title: "Panel" icon: IconType.listul NavigationStack { Page { id: page title: "Panel" AppText { anchors.centerIn: parent text: "Forbot.pl" } } } } NavigationItem { title: "Ustawienia" icon: IconType.suno NavigationStack { Page { title: "Ustawienia" } } } } } Spójrzmy na ten fragment kodu. Pozostaje on bez zmian bez względu na platformę. Wydaje mi się, że nie ma potrzeby na ten moment tłumaczyć tej implementacji. Teraz bardziej interesuje nas efekt: Jak sam widzisz na obu platformach zachowany został dedykowany sposób wyświetlania nawigacji. Podejście to co prawda kuleje, jeżeli nasza nawigacja jest bardziej zaawansowana i znacząco różni się wyglądem od tej natywnej, ale w dalszym ciągu Felgo pozwala programiście na zaoszczędzenie wielu godzin spędzonych na programowaniu tego samego. Zatem Felgo w szczególności nada się przy prototypowaniu aplikacji. Pluginy w Felgo SDK Wiele osób stwierdzi, że jest w stanie poradzić sobie bez typów dostarczonych wraz z Felgo. Dla bardziej wymagających dostępne są pluginy umożliwiające logowanie do aplikacji przy wykorzystaniu kont społecznościowych, monetyzację aplikacji, obsługę powiadomień, integrację z FireBase, raportowanie i statystyki. Wszystkie te rzeczy można zrobić oczywiście samemu, ale wyobraź sobie, ile czasu musiałbyś spędzić na implementację każdej z nich. Nie wszystkie pluginy dostępne są w ramach darmowej licencji Felgo. Na całe szczęście pluginy do monetyzacji aplikacji są dostępne w darmowej wersji, więc możesz zarabiać na swojej aplikacji bez konieczności inwestowania dodatkowych środków. Pełna lista pluginów w Felgo SDK. Czy tworzenie aplikacji mobilnych w Felgo ma sens? Wielu programistów zastanawia się nad tym czy warto oprzeć swoje rozwiązanie na danym frameworku. Na temat tego czy tworzenie aplikacji w Felgo na sens można byłoby napisać dużo porównując go do innych popularnych technologii takich jak React Native. Na własnym przykładzie, mogę powiedzieć, że da się stworzyć w pełni funkcjonalną aplikację korzystając właśnie z Felgo. W moim odczuciu prawdziwym Game Changerem jest kwestia oparcia swojej aplikacji na Qml co pozwala na efektywnie rozgraniczenie logiki od frontendu. Z kolei w kontekście tworzenia samego frontend’u, to Qml pozwala na zachowanie dużej elastyczności. Ta elastyczność jest zbawienna w momencie, gdy nasz projekt graficzny jest dość nietypowy. Na poparcie tych słów umieszczam skrina z aplikacji wykonanej w Qml ze wsparciem Felgo. Zatem wiemy już, że korzystając z Felgo jesteśmy w stanie stworzyć ładną aplikację. Za resztę, czyli za między innymi integrację z zewnętrznymi usługami odpowiadają wspomniane już pluginy. Z pewnością wielu nadal brakuje, ale nie ma technologii idealnej. Słowem podsumowania spójrzmy na grafikę porównującą ilość linii kodu potrzebnych do stworzenia aplikacji PropertyCross. Jak widać w tej statystyce Felgo góruje nad rywalami niepodzielnie, ale oczywiście objętość kodu nie jest ostatecznym wskaźnikiem przy takim wyborze. W samej statystyce nie ujęto też popularnego obecnie Flutter’a. Mimo to, liczby widoczne na tej statystyce wskazują na wysoką efektywność w pracy z Felgo. Efektywność w zawodzie programisty to słowo klucz. Pozwala zaoszczędzić czas, który w branży IT jest przecież czymś na kształt waluty. 😉 Felgo a Game Development Drugą z podstawowych gałęzi działalności Felgo jest wsparcie rozwoju gier, które z powodzeniem można wydać na każdej z obsługiwanych przez Felgo platform. W przypadku tworzenia gier sytuacja wygląda podobnie co do tworzenia aplikacji mobilnych, czyli Felgo SDK rozszerza listę obiektów dostępnych w Qml. Wśród najbardziej interesujących typów znajdują się te odpowiadające za: Backend gry, czyli między innymi: Dostarczanie darmowej chmury przechowującej dane graczy; Synchronizację pomiędzy urządzeniami; Integrację z Facebook’iem; Multi-platformowe rankingi; Wewnętrzny czat; Komponenty wizualne ułatwiające obsługę ekranów o różnych rozmiarach; Fizykę w grze; Przechwytywanie inputu użytkownika; Podstawową sztuczną inteligencję; Efekty dźwiękowe; Felgo SDK potrafi znacznie uprościć i przyśpieszyć pracę nad grą. Dostarczonych komponentów jest na tyle dużo, że bylibyśmy w stanie utworzyć dziesiątki wpisów poświęconych tylko i wyłącznie wykorzystaniu Felgo w Game Development’cie. Na szczęście możemy zacząć od tutoriali przygotowanych przez twórców SDK. Chciałbym w tym momencie nadmienić, że Felgo jest ciekawą alternatywą dla innych frameworków jak chociażby Unity. Oczywiście nad Felgo pracuje znacznie mniej osób, a sama społeczność jest też nieporównywalnie mniejsza. Niemniej pomoc od twórców można uzyskać niemal natychmiastowo. Ciekawą sprawą jest to, że w Unity nie ma dostępnego żadnego pluginu, który dostarczałby gotowe rozwiązanie chmurowe tak jak ma to miejsce w przypadku Felgo Game Network. Właściwie cały backend gry można postawić na chmurze dostarczonej od twórców. Do danych przechowywanych w chmurze i do statystyk gry mamy dostęp z poziomu internetowego panelu. Qml Live Reload – przeładowanie aplikacji w locie Postanowiłem, że tą funkcjonalność zostawię na koniec jako wisienkę na torcie. Felgo udostępnia narzędzie, które pozwala na automatyczne przeładowanie front-end’u aplikacji bez konieczności czekania na koniec kompilacji i uruchamiania na docelowym urządzeniu. Dzięki temu rozwiązaniu jesteśmy w stanie zaoszczędzić mnóstwo czasu w momencie, gdy pracujemy nad kodem napisanym w Qml. Po prostu za każdym razem, gdy zapisujemy dokonane zmiany to aplikacja przeładowuje się dosłownie w sekundę. Dzięki tej funkcjonalności jesteśmy też w stanie testować aplikację równocześnie na wielu urządzeniach bez względu na system operacyjny, na którym pracują. Nie muszę chyba mówić jak bardzo przyśpiesza to pracę. Możliwość przeładowania aplikacji jest szczególnie przydatna w momencie, gdy zmiany, których dokonujemy w kodzie są kosmetyczne. Qml Hot Reload Na obecną chwilę automatyczne przeładowanie aplikacji działa w ten sposób, że przeładowana aplikacja zaczyna z powrotem od głównego pliku Qml. Twórcy Felgo właśnie ulepszyli tą funkcjonalność i od teraz dokonane zmiany widoczne będą w czasie rzeczywistym. Udoskonalona funkcjonalność dostępna będzie w nadchodzącej aktualizacji Felgo w przeciągu kilku tygodni. Korzystać z niej będzie można również gdy docelową platformą projektu są systemy wbudowane. Na poniższym wideo widać jak wygląda praca z Qml Hot Reload: Podsumowanie Mam nadzieję, że po lekturze tego wpisu doskonale wiecie czym jest Felgo SDK oraz jakie są jego mocne strony. Felgo SDK znacznie przyśpiesza pracę nad projektami rozwijanymi w oparciu o Qml. Nawet jeżeli nie interesują Cię komponenty udostępnione przez twórców to z tego SDK warto korzystać chociażby ze względu na Qml Live Reload. Tym bardziej, że ta funkcjonalność nic nie kosztuje. Serdecznie zapraszam do zadawania pytań na temat Felgo SDK. 😊 samples.zip
  20. Cześć, szukam osób, których pasją jest programowanie mikrokontrolerów i które chciałyby zająć się tym tematem na poważnie, lub już się tym zawodowo zajmują. Wymagana podstawowa znajomość języka C lub C++. Oferuję pomoc w przygotowaniu do wymaganego stanowiska. Wymagana jest również znajomość języka angielskiego na poziomie komunikatywnym. Miejsce pracy: Gliwice. Branża: motoryzacyjna. Zainteresowanych proszę o wiadomość prywatną.
  21. Python to język wysokopoziomowy, który ma bardzo szerokie zastosowanie. Można w nim napisać grę (PyGame) albo zrobić komunikację mikrokontrolera z programem na komputerze/laptopie (PySerial), aby na przykład wysyłać komendy. W tym kursie zajmiemy się tą drugą biblioteką. W starszych komputerach istnieją porty szeregowe RS-232. W nowszych komputerach portu tego raczej się nie uraczy. Ten wpis brał udział konkursie na najlepszy artykuł o elektronice lub programowaniu. Sprawdź wyniki oraz listę wszystkich prac » Partnerem tej edycji konkursu (marzec 2020) był popularny producent obwodów drukowanych, firma PCBWay. Jest jednakże światełko w tym ciemnym tunelu, gdyż sterowniki niektórych urządzeń USB emulują port szeregowy COM umożliwiając tym samym proste komunikowanie się z takim urządzeniem na nowszych maszynach. Do takich urządzeń należą płytki rozwojowe Arduino RS-232(rys. 1) Prosta komunikacja pomiędzy uC (mikrokontrolerem) a PC (rys. 2) 1.Wysyłanie informacji z mikrokontrolera do komputera/laptopa. Konfiguracja połączenia z portem COM Zanim zacznie się przygodę z komunikacją za pośrednictwem portu COM konieczne jest zapoznanie się z podstawami jego działania. Port ten przesyła dane dwukierunkowo za pomocą jednego pinu przesyłającego i jednego odczytującego dane. Dane przesyłane są zawsze z określoną prędkością mierzoną w bitach na sekundę. Standardowe ustawienie prędkości transmisji z urządzeniem wynosi 9600 bitów na sekundę. Ważne aby, wysyłać i odbierać dane z taką samą częstotliwością w przeciwnym przypadku dane nie będą odbierane przez urządzenie w sposób poprawny jak również program nie będzie w stanie poprawnie odbierać danych. Przy podstawowej konfiguracji konieczne jest również posiadanie wiedzy o nazwie portu. Pod Windowsem nazwy portów zaczynają się od COM i kończą liczbą określającą numer portu. Można sprawdzić w systemie, jakie porty COM są dostępne w Menadżerze urządzeń co też i widać na poniższym rysunku (rys. 3) rys. 3 Przygotowanie środowiska na komputerze/laptopie Tak jak już mówiłem, będziemy potrzebować biblioteki PySerial omówię jej instalację w środowisku PyCharm: Wchodzimy w terminal, następnie wpisujemy: "pip install pyserial" Naciskamy enter Powinniśmy zobaczyć coś takiego (rys. 4) rys. 4 teraz przejdzmy do Arduino. Przygotowywanie Arduino (oczywiście zadziała komunikacja zadziała wszędzie gdzie użyjemy UART'a, nie tylko Arduino) Na razie jedyne co napiszemy to: void setup() { Serial.begin(9600); // Ustawienie Baud Rate(prędkość transmisji) na 9600Hz } void loop() { Serial.println("Proba Komunikacji"); delay(1000); } Wgrywamy nasz program, uruchamiamy Monitor Portu Szeregowego gdzie powinno sie pojawić się (rys. 5) rys. 5 i tak co około sekundę (przy okazji widzimy, że funkcja delay nie jest taka dokładna (dlatego nie stosuje sie jej gdy robimy na przykład zegar)) 😁 Teraz można przejść do PyCharma import serial arduino = serial.Serial('COM5', 9600, timeout=0.1) while True: data = arduino.readline() if data: data = data.decode() print(data) Można powiedzieć, że właśnie zrobiliśmy monitor portu szeregowego z ArduinoIDE 😀 Omówienie kodu: Importujemy bibliotekę, Ustawiamy port do któego mamy podłączone Arduino oraz Baud Rate, Przypisujemy do zmiennej to co aktualnie jest przesyłane, Jeżeli zmienna nie jest pusta to ją dekodujemy i wyświetlamy na ekranie. ZAWSZE MUSIMY PAMIĘTAĆ O ZDEKODOWANIU (tylko na komputerze)!!! Wyłączamy monitor portu szeregowego (ten z ArduinoIDE), kompilujemy program i naszym oczom powinno ukazać się (rys. 6) rys. 6 2. Wysyłanie komend z komputera/laptopa do mikrokontrolera. Przejdźmy do PyCharma import serial import time arduino = serial.Serial('COM5', 9600, timeout=0.01) while True: arduino.write('wlacz'.encode()) time.sleep(1) arduino.write('wylacz'.encode()) time.sleep(1) Importujemy bibliotekę time (nie trzeba jej instalować) oraz wysyłamy "wiadomości": "wlacz" oraz "wylacz" To by było na tyle w PyCharmie, przejdźmy więc do ArduinoIDE rys. 7 Jako że robimy komunikację używając UART'a, który może wysyłać maksymalnie jeden znak, ponieważ (rys. 7) jeden znak to jeden bajt (bajt ma 8bitów) a my wysyłamy komendy: "wlacz" oraz "wylacz" to musimy zrobić taki mały myczek elektryczek 😀 i użyć zmiennej oraz pętli. Będzie wyglądać to tak: wysyłamy: w Arduino: "odbiera" i zapisuje do zmiennej wysyłamy: l Arduino: "odbiera" i zapisuje do zmiennej wysyłamy: a Arduino: "odbiera" i zapisuje do zmiennej wysyłamy: c Arduino: "odbiera" i zapisuje do zmiennej wysyłamy: z Arduino: "odbiera" i zapisuje do zmiennej nie wysyłamy nic Arduino: wychodzi z pętli oraz porównuje zawartość zmiennej z komendami które ma zapisane Arduino: wykonuje komendę Arduino: czyści zawartość zmiennej z komendą Takie wybrnięcie z sytuacji 😎 int i = 12; //pin do którego podłączymy diodę String komenda=""; void setup() { Serial.begin(9600); pinMode(i, OUTPUT); digitalWrite(i, HIGH); } void loop() { if(Serial.available() > 0) { while(Serial.available() > 0) { komenda += char(Serial.read()); } Serial.println(komenda); if(komenda == "wlacz") { digitalWrite(i, HIGH); } if(komenda == "wylacz") { digitalWrite(i, LOW); } komenda = ""; } delay(100); } Oczywiście można wysłać do komputera/laptopa "informację zwrotną" na przykład: Dioda jest wlaczona Dioda jest wylaczona Tylko musimy pamiętać aby użyć .decode(), ale tak jak mówiłem, tylko w programie na komputrzez/laptopie Jeżeli nasze komendy będą miały tylko 1 znak na przykład: a, A, 1, B, c, C ,4 (ogólnie to dowolny znak z tabeli ASCII) nie trzeba używać pętli tylko: if(Serial.read() == 's') oczywiście też w tym ifie: if(Serial.available() > 0) Jeżeli wstawilibyśmy tam więcej znaków dostalibyśmy taki komuniukat: warning: multi-character character constant [-Wmultichar]. 3. Podsumowanie Wysyłanie oraz odbieranie informacji jest bardzo przydatne, nie musi być to tylko pomiędzy uC, a komputerem. Może to być komunikacja pomiędzy dwoma uC na przykład: karta microSD ma w sobie procesor który komunikuje sie z uC używając SPI, termometr DS18B20 który komunikuje z uC używając protokołu komunikacji OneWire (rys. 8), czujnik podczerwieni, procesor w naszych komputerach z mostkiem, pamięcią i GPU, ładowarka z telefonem, aby ustalić jaki prąd i napięcie. Komunikacja jest wszędzie (można powiedzieć, że urządzenia są bardziej komunikatywne od nas 🤣). rys. 8
  22. Czesc, Mam problem z programem a mianowicie nie działa mimo poprawnej składni. Konsultowałem go już z kilkoma osobami, jednak nikt nie był mi w stanie pomóc. W C działa jak jest zapisany w programie a w C++ w ogóle nie mogłem go odpalić. Pomogliście mi w 2014 to mam i nadzieję że pomożecie dzisiaj ;D #include <stdio.h> #include <stdlib.h> //Generator liczb pierwszych do 535 liczb. //Maksymalna deklaracja tablicy 124 999 999; //Dziala do zakresu 3856 - powyzej sie zaawiesza. //Wyswietlanie wszystkich liczb spowalnia //Jednakowo dziala dla CodeBlock i Dev unsigned long int tab[8048]; unsigned long int pierwsza[8048]={2,3}; //Poczštkowy zbior dwoch liczb int main() { //deklaracja zmiennych unsigned long int a,b,c,m,i,p,n; //podanie zakresu do jakiego ma szukac printf("Podaj wartosc zakresu dla wyznaczenia liczb :"); scanf("%lu", &m); //Wypelnienie tablicy liczbami z zakresu for(i=0;i<=m;i++) { tab=i; } //Wartosci poczatkowe a=1; //zakres b=2; //Ilosc liczb pierwszych c=1; while(c<=m) { //wyznaczenie zakresu na podstawie zaleznosci a=pierwsza[b-1]*2; //zerowanie w tablicy wartosci nie bedacych pierwszymi //dla danego zakresu for(i=0;i<b;i++) { n=2; while(pierwsza * n<=a) { p = pierwsza * n; tab[p]=0; n++; } } //policzenie liczb pierwszych b=0; for(i=2;i<=a;i++) { if (tab!=0) { b++; } } //Wpisanie do tablicy nowych liczb pierwszych n=0; for(i=2;i<=a;i++) { if(tab!=0) { pierwsza[n]=tab; n++; } } //c=pierwsza[b-1]; >> tak powinno być c=pierwsza[b-1]+4; //działa tylko dla 4 i więcej } printf("Liczby to:\n"); //wyswietlenie liczb for(i=0;i<b;i++) { printf("%lu\n", pierwsza); } printf("\nNajwieksza liczba pierwsza %lu\n", pierwsza[b-1]); printf("\nLiczb pierwszych jest:%lu\n", b); system("PAUSE"); return 0; }
  23. Cześć, każdy z nas musi od czasu do czasu odświeżać sobie znane już wiadomości z używanego języka programowania (szczególnie, że "na co dzień" nie używamy wszystkich z zaawansowanych konstrukcji języka) lub uczyć się nowych rzeczy dla szybko zmieniających się standardów. Ja "na co dzień" używam języka C (nieobiektowego ponieważ w programowaniu mikro-kontrolerów nadal rzadko korzysta się z C++). Pomyślałem sobie, że fajnie byłoby na forum, gdybyśmy co jakiś czas zadawali sobie pytania dotyczące programowania w Językach C/C++ dotyczące jakichś trudniejszych do zrozumienia konstrukcji języka. Oto moje pierwsze pytanie: Załóżmy, że mamy definicję tablicy tab: int tab[5][4] = { {23, 2, 7, 1}, {4, 1}, {[2]=2, 11}, {[1]=27, 0, 7}, {9, 2, 1} }; Kto wie jaka będzie wartość wyrażenia: *(*(tab+2)+1) Zakładamy, że używamy kompilatora C zgodnego przynajmniej ze standardem C99 np. popularnego gcc (Linux, można go też uzywać pod Windows instalując pakiet "mingw"). Proszę, o krótkie uzasadnienie teoretyczne odpowiedzi (ponieważ można napisać prosty program i sprawdzić wartość). Czekam też na ciekawe pytania dotyczące programowania w C/C++ od Was 😉 Pozdrawiam
  24. Poszukujemy wykwalifikowanych inżynierów, którzy zdobyli doświadczenie w tworzeniu oprogramowania dla układów wbudowanych i chcą je pogłębić pracując na projektami w branży motoryzacyjnej. Jeżeli lubisz samochody, chcesz codziennie widzieć wyniki swojej pracy obserwując pojazdy na drogach całego świata dołącz do naszego zespołu! 🙂 Jeśli jesteś osobą, która posiada: Wykształcenie wyższe (elektronika, automatyka, informatyka, telekomunikacja lub pokrewne) Bardzo dobrą znajomość języka C Umiejętność tworzenia oprogramowania dla systemów wbudowanych (doświadczenie komercyjne 5+ lat) Znajomość architektury i zasady działania mikroprocesorów Dobrą znajomość języka angielskiego Znajomość systemów operacyjnych czasu rzeczywistego Dlaczego warto do nas dołączyć: U nas rozwijasz swoją pasję do motoryzacji, pracując nad najnowszymi technologiami, które dopiero pojawią się na drogach Bierzesz udział w długoterminowych projektach realizowanych dla światowych marek samochodowych Oferujemy możliwość nieustającego doskonalenia swoich umiejętności i wiedzy Ciągły kontakt z najnowszymi technologiami z dziedziny samochodowych systemów wbudowanych Krótki proces rekrutacyjny - 1 rozmowa F2F Atrakcyjne widełki finansowe 15 000 - 18 000 PLN netto
×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.