Skocz do zawartości

Tablice w Arduino – #5 – znaki, cstring


Pomocna odpowiedź

W ostatniej części kursu przećwiczyliśmy różne sposoby deklaracji i inicjalizacji tablic oraz nauczyliśmy się poruszać się po tablicy używając wskaźników. W tej  części kursu zajmiemy się tablicami przechowującymi znaki.

rysunki_6.thumb.jpg.dbb643f31b80d66c81e3ff53973e2159.jpg

Spis treści

To już było

Dla tych którzy już mają doświadczenie z Arduino i wiedzą jak wypisać napis używając portu szeregowego temat ten może wydać się oczywisty – bierzemy funkcję println (a dokładnie metodę z klasy Print, dziedziczonej przez Stream i wreszcie przez znaną nam klasę Serial) wpisujemy tekst i gotowe:

Serial.begin(9600);
Serial.println("Hello world");

Na tym jednak sprawa się nie kończy – jest jeszcze wiele tematów, które warto poznać.

Czym jest napis?

Podstawowy napis w C i C++ nazywa się string lub C-string i jest to tablica znaków. Zamiast pisać każdy znak po przecinku:

char sentence[30] {'U', 'N', 'O', ' ', ..., '.'};

możemy wykorzystać napis ujęty w cudzysłowy:

char sentence[30] {"UNO is turquoise."};

Każdy znak napisu jest zakodowany w tzw. kodzie ASCII, który może przyjąć 256 wartości (od 0 do 255) bez znaku. W tych znakach można znaleźć m.in.:

  • małe litery od 'a' do 'z' o kodach od 97 do 122 włącznie,
  • wielkie litery od 'A' do 'Z' o kodach od 65 do 90 włącznie,
  • białe znaki:
    • spację (kod 32 lub szesnastkowo 0x20),
    • tzw. null czyli koniec napisu (kod 0 lub 0x0).

Napisy ujmujemy w cudzysłowach(np. "napis"), ale znaki w apostrofach (np. 'a').

Wymieniłem najważniejsze znaki, których będziemy szukać w kolejnym przykładzie. Listę wszystkich znaków można prześledzić w tabeli:

t7.thumb.png.93bd126d9c7580012da7d5c99984694d.png

Wracając do przykładowego napisu, wypiszmy wszystkie znaki, ich indeksu w tablicy i wartości (dlatego, że tzw. znaki białe są niewidoczne). Kod może wyglądać tak:

constexpr int SENTENCE_BUFFER_SIZE = 30;
char sentence[SENTENCE_BUFFER_SIZE] {"UNO is turquoise."};

void setup() {
  Serial.begin(9600);

  for (int i = 0; i < SENTENCE_BUFFER_SIZE; ++i) {
    Serial.print(i); Serial.print(F(": ")); Serial.print(sentence[i]); Serial.print(F(" / 0x")); Serial.println(sentence[i], HEX);
  }
}

W kodzie pojawia się w nazwie stałej BUFFER. Często tablice służące do przekazywania danych nazywane są buforami, w tym przypadku tablica znaków jest dużo dłuższa od napisu, więc można uznać, że nie jest ona stałym napisem, tylko buforem do wysyłania – nazewnictwo zależy tu od implementacji.

Ciut długa ta linia wypisująca, ale wrócę do tego dlaczego nie da się czegoś z tym zrobić (albo raczej nie ma sensu próbować). Poza tym raczej nic zaskakującego. W wyniku otrzymamy:

0: U / 0x55
1: N / 0x4E
2: O / 0x4F
3:   / 0x20
4: i / 0x69
5: s / 0x73
6:   / 0x20
7: t / 0x74
8: u / 0x75
9: r / 0x72
10: q / 0x71
11: u / 0x75
12: o / 0x6F
13: i / 0x69
14: s / 0x73
15: e / 0x65
16: . / 0x2E
17:  / 0x0
18:  / 0x0
...
29:  / 0x0

Jak widzimy udało się wypisać poszczególne znaki. Znaki białe również są widoczne dzięki wypisaniu ich kodów i tak jak zapowiedziałem spacja ma kod 0x20. Na końcu tablicy mamy sporo znaków o kodzie zero (tzw. null). W sumie nie ma co się dziwić, skoro tablica została zainicjalizowana globalnie, to niewykorzystane elementy są tak traktowane (mają wartość 0).

W przypadku tablic znaków, białe znaki null na końcu niosą dodatkową informację – jako, że znak null (czyli wartość 0) nie koduje pisanego znaku, a tylko wypełnia pustą, końcową przestrzeń to jego występowanie można wykorzystać do stwierdzenia końca napisu i to bez znajomości długości napisu – dlatego w funkcjach służących do wypisywania napisów nie występuje zmienna określająca długość. Spróbujmy wykorzystać tę wiedzę, do pominięcia zbędnych elementów bufora napisu:

  int i = 0;
  
  while (sentence[i] != '\0') {
    Serial.print(i); Serial.print(F(": ")); Serial.print(sentence[i]);Serial.print(F(" / 0x")); Serial.println(sentence[i], HEX);
    ++i;
  }

Sprawdzamy czy aktualnie wskazywany element tablicy ma wartość null – w C była do tego dyrektywa NULL, w C++ zastąpiono go null (pisanymi małymi literami), który co prawda pojawia się w słowach kluczowych Arduino IDE, ale nie odnosi się do niczego.

Warto też pamiętać, że w C++11 występuje wskaźnik nullptr, który wskazuje na zerowy adres. Służy on do wykrywania błędów w użyciu wskaźników.

Powyższy zapis można uprościć, znak '\0' posiada wartość 0, więc w warunkach jest traktowany jak fałsz

  while (sentence[i]) { \*...*\ }

Tablica to nie zawsze C-string

W poprzednim przykładzie przygotowaliśmy bufor znaków dużo za duży jak na tak krótki napis. Sprawdźmy co się stanie, gdy użyjemy bufora znaków skrojonego na miarę (posiadającego rozmiar równy liczbie znaków):

char suspicious_sentence[16] {"It's suspicious."};

Próbując wypisać taki napis otrzymamy błędy:

image.png.20c3646395288c4db11bfac7e04337ed.png

Jak zauważyliśmy wyróżnikiem C-stringa jest to, że posiada na końcu terminator  przynajmniej jeden znak null o wartości 0 (lub '\0'), który niesie informację o końcu napisu. W sytuacji, gdy tablica posiada rozmiar równy liczbie wypisywanych znaków nie da się dodać na końcu znaku null – nie jest wiec C-stringiem tylko zwykłą tablicą znaków.

Wystarczy więc albo zwiększyć rozmiar bufora o 1, albo zdefiniować go z pominięciem rozmiaru – w ten sposób rozmiar zostanie dobrany automatycznie o 1 większy od napisu:

char suspicious_sentence[17] {"It's suspicious."};

/* lub */

char suspicious_sentence[] {"It's suspicious."};
Serial.println(sizeof(suspicious_sentence)); // 17

Ma to sens, ponieważ nie zawsze tablica znaków służy do wypisywania znaków.

image.thumb.png.87605398a99ef9aa401434f784c9cfbc.png

Więcej niż C-string

W praktyce korzystania z Arduino często posługujemy się inną składnią – jakieś string (a może String), F(), większość i tak wypisujemy od razu w monitorze portu szeregowego używając funkcji print() i println(). Jak to działa?

C-string lub string (z małej litery) odnosi się do tablicy znaków ze znakiem null na końcu. Podobnie napis podany w cudzysłowach przekazany do funkcji println też jest tego typu napisem.

Zaletą (a zarazem wadą) C-stringów jest to, że są zapisane statycznie – w końcu są statyczną tablicą. Oznacza to, że ich rozmiar musi być znany na etapie kompilacji. Dzięki temu można je szybko wypisać, a w przypadku problemów z pamięcią operacyjną, można użyć funkcję F() zapisującą stałe dane tablicy do pamięci FLASH.

Dobrą praktyką podczas nauki jest sięganie po dokumentację i analiza kodu napisanego przez innych, bardziej doświadczonych programistów. O ile analizę bibliotek Arduino pochodzących od zewnętrznych wydawców lepiej pozostawić "saperom programowania" to z analizy głównej biblioteki Arduino można się czegoś nauczyć.

Biblioteka Arduino (Core dla AVR) jest na GitHubie. W folderze cores/arduino znajdziemy kod źródłowy klas i funkcji.

Jeżeli przejrzymy zawartość plików biblioteki to na pewno znajdziemy tam wspomniany już plik Print.h, w którym są funkcje wypisujące. Jest tam funkcja do wypisywania zwykłych C-stringów podanych w argumencie jako tablica (więcej o tablicach w funkcjach w kolejnej części):

size_t print(const char[]);

Funkcja print jest przeładowana, tzn. istnieje wiele wariantów tej samej funkcji różniących się argumentami i implementacją.

Jest także nieco trudniejszy wariant funkcji, przyjmujący argument związany z funkcją F() lokującą napis w pamięci FLASH – interpretacja tego jednak wykracza poza ten kurs:

size_t print(const __FlashStringHelper *);

Jest też argument związany z klasą String  pisaną z wielkiej litery:

size_t print(const String &);

Pisząc o C-stringu podałem, że ma on stały rozmiar. Jest to też jego wada, ponieważ czasem potrzeba wypisać wiadomości o różnej długości. Zazwyczaj w takich sytuacjach definiuje się bufor znaków o największym możliwym rozmiarze, tak by pomieścić każdy przypadek wiadomości, jest on zarezerwowany w pamięci statycznej RAM i na etapie kompilacji wiemy, na co możemy sobie pozwolić. Istnieje jednak sposób tworzenia napisów, których rozmiar może zmieniać się w czasie działania kodu.

image.thumb.png.b1be4c0d1624ed8ff0d0845d8ebe569d.png

Klasa String

W końcu weszliśmy obiema nogami w pojęcie klasy, którego starałem się jakoś uniknąć. Tak naprawdę wchodzimy też w temat tablic dynamicznych i jeszcze większego bagna wskaźników, ale postaram się przekazać istotę tematu.

To co bardzo odróżnia C++ od C to istnienie klas czyli takich jakby typów danych o dużo większych możliwościach od zwykłych typów prostych. W C można stworzyć "twór klasopodobny" przy użyciu struktury ze wskaźnikami na funkcje ale to nie jest do końca to.

Klasa posiada oddzielony zestaw zmiennych (tzw. pól) oraz własnych funkcji (tzw. metod), które związane są z klasą lub jej instancjami (czyli obiektami tej klasy). Zagmatwane ale wyjaśniam: klasa może mieć jakąś swoją logikę, która nie powinna być widoczna dla użytkownika. Posługując się przykładem to coś jak budowa komputera – nie widzisz warstwy sprzętu, procesora itp. tylko efekt działania na monitorze. Masz za to wyprowadzone funkcje, którymi możesz odwoływać się do tego co widzisz – poruszanie wskaźnikiem myszy, wpisywanie znaków klawiaturą.

String (pisany z wielkiej litery) jest klasą, czyli takim typem danych, z którym możemy robić coś więcej niż zer zwykłą tablicą. Instancję (czyli obiekt/pojedyncze wystąpienie reprezentanta klasy) definiujemy następująco:

String advanced_string = String("UNO is turquoise ");

Biblioteka związana z napisem jest domyślnie włączona do kodu Arduino.

Na razie zapamiętaj tę formę zapisu – w ten sposób w ogólności powołuje się do życia obiekty klas. Element po prawej choć nazywa się tak samo jak klasa to jest funkcją i to specjalną. String() jest konstruktorem, czyli funkcją wywoływaną przy tworzeniu nowego obiektu. W tym przypadku do wnętrza konstruktora przekazujemy napis (w otoczeniu cudzysłowów, czyli tak jak w C-stringu).

Póki co nie widać specjalnych różnic z C-stringami, taki napis można tak samo wypisać używając port szeregowy (i wspomnianego wariantu funkcji (a dokładnie metody) println():

void setup() {
  Serial.begin(9600);
  Serial.println(advanced_string);
}

Prawdziwe "czary" zaczną się, gdy sięgniemy po narzędzia klasy String.

Narzędzia obiektów (instancji) klas, albo w ogólności klas, nazywamy metodami. Są to funkcje skojarzone z konkretną klasą.

Jedną z funkcjonalności jest przeciążony operator dodawania. Co to oznacza? Tyle, że możemy dodawać do siebie różne rodzaje napisów:

String advanced_string = String("UNO is turquoise ");

void setup() {
  Serial.begin(9600);
  advanced_string += "which is mix of blue and green.";
  Serial.println(advanced_string);
}

W tym przypadku do obiektu klasy String dodaliśmy C-string. Co będzie wypisane?

UNO is turquoise which is mix of blue and green.

Napis został rozszerzony! Bez zabawy w znaki null, rozmiary tablic, pamięć... właśnie, a gdzie to się w ogóle zapisało?

image.thumb.png.91a03a9a4e911282fb7c65d0d0f147e8.png

Dygresja o IDE

Arduino IDE jest świetne do nauki, ale takiej bardzo elementarnej. Sam często do niego wracam jak chcę jednym kliknięciem założyć projekt. Ale gdy chcemy przyjrzeć się implementacji klas czy funkcji warto jest sięgnąć po coś lepszego. Serdecznie polecam Visual Studio Code i plugin PlatformIO, dzięki którym programowanie Arduino jest usprawnione, kompilacja szybsza (bo część kodu kompilowana jest w locie), kolorowanie składni bardziej trafne i jak dla mnie najważniejsza funkcjonalność niezbędna w nauce – łatwe poruszanie się po kodzie i bibliotekach.

Przykładowo chcę dowiedzieć się czegoś o klasie String, zamiast szukać pliku w drzewie gita, otwieram Visual Studio Code, piszę przykładową instancję klasy String:

image.thumb.png.acf4f9b4596c47502352edd682c083b3.png

Tylko najechałem myszą na nazwę klasy i już wiem co to jest (jaką ma przestrzeń nazw i jakiś opis). Ale teraz klikam na to LPM  trzymając Ctrl i przechodzę do kodu źródłowego klasy, a dokładnie pliku String.h:

class __FlashStringHelper;
#define F(string_literal) (reinterpret_cast<const __FlashStringHelper *>(PSTR(string_literal)))

// An inherited class for holding the result of a concatenation.  These
// result objects are assumed to be writable by subsequent concatenations.
class StringSumHelper;

// The string class
class String
{

image.thumb.png.3ce66e00af618df6fb62581a59e84675.png

Świadome używanie klasy String

Po tej krótkiej dygresji sprawdźmy co siedzi w klasie String, że choć jest tak genialna to ma dość złą sławę. Otwieramy implementacje konstruktora dla argumentu w postaci stałej tablicy znaków C-string:

String::String(const char *cstr)
{
	init();
	if (cstr) copy(cstr, strlen(cstr));
}

Przechodzimy do funkcji init(), gdzie znajdujemy inicjalizację pól (zmiennych) klasy:

inline void String::init(void)
{
	buffer = NULL;
	capacity = 0;
	len = 0;
}

Słowo kluczowe (czy tu specyfikator) inline oznacza, że wstawienie kodu funkcji będzie preferowane (niż typowe przejście do funkcji). Wychodzimy z init().

Druga linia konstruktora jest ciekawsza, możemy ją odczytać tak: jeżeli wskaźnik tablicy nie wskazuje adresu zerowego nullptr (zawarte jest to w tym uproszczonym warunku) to wykonaj funkcję copy(). W argumentach widzimy przekazanie tablicy i jej długości (do której użyto funkcję strlen() z biblioteki string.h).

Idziemy więc do funkcji copy() i tam znajdujemy:

String & String::copy(const char *cstr, unsigned int length)
{
	if (!reserve(length)) {
		invalidate();
		return *this;
	}
	len = length;
	strcpy(buffer, cstr);
	return *this;
}

Niewydłużając przechodzimy do funkcji reserve() i dalej do funkcji changeBuffer() i tam mamy coś co może nas bardziej zaciekawić:

unsigned char String::changeBuffer(unsigned int maxStrLen)
{
	char *newbuffer = (char *)realloc(buffer, maxStrLen + 1);
	if (newbuffer) {
		buffer = newbuffer;
		capacity = maxStrLen;
		return 1;
	}
	return 0;
}

Funkcja ta jest podobna do wspomnianej funkcji init() tylko tym razem ustawiamy rozmiar bufora. W części kursu poświęconej pamięci RAM wspomniałem o obszarze pamięci dla użytkownika – sterta (ang. heap), która w porównaniu ze stosem jest poszatkowana przez swobodną alokację i zwalnianie pamięci. To co widzimy tu to właśnie zapisanie zmiennych dynamicznych na stercie przy pomocy funkcji realloc().

Funkcja realloc() służy do przenoszenia danych w pamięci – w sytuacji, gdy obecny blok danych jest za mały możemy go rozszerzyć nie naruszając ciągłości danych. W takiej sytuacji zostanie zarezerwowany nowy, spójny obszar pamięci (niekoniecznie zaczynający się od tego samego adresu) do którego przekopiowane zostaną dane z poprzedniego bloku.

image.thumb.png.7dd37f8723750244e637812052d79171.png

Jak można się domyśleć realloc() zajmuje więcej operacji niż wpisanie danych do statycznego bufora o większym rozmiarze. Poważne schody zaczynają się, gdy pomyślimy o tym jak co pozostawia po sobie funkcja przenosząca dane – gdy zwalniany jest jeden obszar, a zajmowany inny może powstać dziura w pamięci. Nie jest to samo z siebie złe, ale gdy takich dziur jest dużo, to poszatkowanie sterty może doprowadzić, że nie uda się zaalokować spójnego obszaru na nowe dane i cała sterta "się rozsypie".

image.thumb.png.27cfdfd1c46708dc2f82ccd1d65d29a6.png

Zatem klasa String jest przydatna, ale trzeba używać jej z głową. Są sytuacje gdzie się przydaje, ale na mikrokontrolerze z 2 kiB RAMu niewiele zdziałamy. Właśnie, a ile można?

Zróbmy sobie debugger

Najprościej byłoby skorzystać z debuggera, jednak na ten wariant Arduino (ATmegi) nie mamy takiego. Napiszemy wiec kod analizujący zużycie pamięci.

Na początek możemy spróbować utworzyć najdłuższy możliwy String. Tu warto dopowiedzieć, że konstruktor jest też przeciążony i można podać sam napis tworząc nowy obiekt String:

void setup(void) {
  Serial.begin(9600);
  String s = String("basdasdb ... 9023412");
  Serial.println(s.length());
}

Metodą prób i błędów możemy oszacować ile mniej więcej uda się upchnąć. U mnie wyszło około 756 znaków. Dla tablicy statycznej C-string wynik to około 793. Nie jest to wielka różnica, w większości wynika z narzutu danych samej klasy String, ale też nie to jest istotne. Sprawdźmy co się dzieje gdy przeprowadzane są operacje na obiektach String.

Aby śledzić na bieżąco stopień zajęcia pamięci można skorzystać z biblioteki FreeMemory, która posiada 2 funkcje:

  • freeListSize() informującą o liczbie wolnej pamięci począwszy od początku sterty do aktualnego wolnego miejsca w pamięci (innymi słowy wszystkie dziury),
  • freeMemory() - wolną pamięć od aktualnego miejsca do limitu.

Przykładowy kod inspirowany tą stroną może wyglądać następująco:

#include <MemoryFree.h>

void reportMemory(int iteration, int loop_lap, int datalen)
{
  Serial.print(F("free:"));
  Serial.print(freeMemory());
  Serial.print(F(","));

  Serial.print(F("list:"));
  Serial.print(freeListSize());
  Serial.print(F(","));

  Serial.print(F("iteration:"));
  Serial.print(iteration);
  Serial.print(F(","));

  Serial.print(F("lap:"));
  Serial.print(loop_lap);
  Serial.print(F(","));

  Serial.print(F("datalen:"));
  Serial.print(datalen);
  Serial.println("");
}

void test(int max_datalen)
{
  String s = "";
  static int iters;

  for (int i = 0; i < max_datalen; i++)
  {
    s = s + ('0' + (i % 10));
    if (s)
      reportMemory(iters, i, s.length());
    else {
      reportMemory(iters, i, 0);
    }
  }
  iters++;
}

void setup(void) {
  Serial.begin(9600);
}

void loop(void)  {
  test(400);
}

Test polega na dołączaniu do ciągu znaków kolejnych cyfr. Jak wynika z analizy kodu źródłowego, dane będą co jakiś czas realokowane pozostawiając po sobie wolne przestrzenie. Jeżeli wyświetlimy odczyty portu szeregowego na tzw. "kreślarce" będziemy mogli zobaczyć wykres jak poniżej:

t9_old.thumb.png.86671808d92281ce80c3556cba9bc31f.png

Na poziomej osi mamy obiegi pętli – od 0 do 400 mamy jeden test. Niebieski wykres oznacza dostępną pamięć, zaś poszarpany czerwony przebieg oznacza wolną pamięć sterty od początku sterty do aktualnej pozycji testowej. Różowy wykres reprezentuje długość stringa, zaś pomarańczowy informuje o trwaniu testu – aktualnej iteracji.

Jak można zauważyć, gdy doliczy się do końca testu, dane zostają zwolnione – użycie pofragmentowanej pamięć spada do 0, wolna pamięć skacze do maximum, a długość stringa wynosi 0. Jest to poprawne zachowanie przy zwalnianiu pamięci.

Gorzej, że to samo zjawisko wystąpiło w środku testu w zupełnie niespodziewanym momencie... Choć dysponowaliśmy dość sporym obszarem wolnej pofragmentowanej pamięci to nie dało się widocznie jej użyć i przy próbie alokacji nowej nastąpiło przepełnienie i dane zostały utracone. Jedyna opcja na wychwycenie tej anomalii to sprawdzanie czy obiekt został nieplanowanie wyczyszczony:

    if (s)
      reportMemory(iters, i, s.length());
    else {
      reportMemory(iters, i, 0);
    }

Jak można zauważyć dane alokowane w taki sposób były dużo krótsze od tego co zajęliśmy przy "czystej stercie". Po 181 dodawaniu pamięć się przepełniła:

free:1294,list:732,iteration:0,lap:179,datalen:360
free:1292,list:732,iteration:0,lap:180,datalen:362
free:1290,list:732,iteration:0,lap:181,datalen:364
free:1657,list:0,iteration:0,lap:182,datalen:0
free:1652,list:5,iteration:0,lap:183,datalen:2

Dla zainteresowanych analizą pamięci polecam stronę opisującą bibliotekę memdebug, która posiada też dodatkowe funkcje, ale też przy jej użyciu da się uzyskać podobny wykres:

image.thumb.png.6c27c711f87a58a2a5de70f89b467d1f.png

W niektórych miejscach kod jest praktycznie identyczny z poprzednio opisywaną biblioteką, dlatego można w ramach samorozwoju porównać obie. 

Co z tym printem?

Trochę długa ta część... ale jeszcze jedno. Dlaczego podczas wypisywania czegoś do portu szeregowego nie łączy się napisów, zmiennych, itp. w jeden ciąg?

Serial.print(i); Serial.print(F(": ")); Serial.print(sentence[i]);Serial.print(F(" / 0x")); Serial.println(sentence[i], HEX);

Odpowiedź może być prosta: bo po co? Gdyby połączyć fragmenty tekstu to trzeba wykonać te operacje, trzeba zaalokować pamięć. Chyba najgorszym możliwym sposobem byłoby łączeniem danych przed dodawanie Stringów, przy czym operacje te są dość karkołomne, bo dodawać można tylko obiekt String do czegoś innego, a najlepiej to zamienić wszystkie inty, floaty itp na łańcuchy znaków i wtedy pododawać. Szczegóły w dokumentacji, ale zapewniam, że to zły pomysł.

image.thumb.png.1f3d5a16457afe4570237ef0b6b5dad3.png

Inna opcja to zapisanie danych do bufora używając funkcji sprintf(). Przykład z początku będzie wyglądał tak:


char buff[100];

constexpr int SENTENCE_BUFFER_SIZE = 30;
char sentence[SENTENCE_BUFFER_SIZE] {"UNO is turquoise."};

void setup() {
  Serial.begin(9600);

  int i = 0;
  
  while (sentence[i] != '\0') {
    sprintf(buff, "%d: %d / 0x%02X", i, sentence[i], sentence[i]);
    Serial.println(buff);
    ++i;
  }
}

Jak widać potrzebny jest bufor na tymczasowe znaki. Przy użyciu funkcji sprintf() łączymy napis ze zmiennymi stosując się do umieszczonych znaków specjalnych, zapisujemy wynik do bufora i wypisujemy znaną funkcją println(). Wynik taki jak poprzednio. Czy jest to warte?

Funkcja print() bazuje na funkcji write(), która wysyła kolejne znaki do buforu UART, a tam sprzętowo są wysyłane – nie ma tu więc dodatkowych zmiennych (za wyjątkiem tych wchodzących w implementacje standardowych bibliotek). Dodatkowo można użyć funkcji F(),  która alokuje napisy w pamięci FLASH. Biblioteka Arduino posiada też możliwość wypisania zmiennych float.

Można pokusić się o zapis danych do tablicy, jeżeli wiemy, że będą to powtarzalne dane, których nie jesteśmy wstanie ustalić na etapie kompilacji.

Podsumowanie

W tej dość długiej części kursu zapoznaliśmy się z tablicami znaków. Jak się okazuje, nie jest to tak oczywisty temat więc warto przyjrzeć się zarówno C-stringom, klasie String oraz bibliotekom pokrewnym.

W kolejnej części zajmiemy się tematem przekazywania argumentów funkcji, aby mieć zestaw narzędzi do zmierzenia się z kolejnym praktycznym przykładem.

 

Edytowano przez Gieneq
  • Lubię! 1
Link to post
Share on other sites
  • Gieneq zmienił tytuł na: Tablice w Arduino – #5 – znaki, cstring
(edytowany)

Kolejny tydzień, kolejna część kursu. Tym razem dość sporo treści. Warto jest pobrać bardziej rozbudowane środowisko programistyczne, w którym będziemy mogli podejrzeć implementację standardowych bibliotek. 📚

Edytowano przez Gieneq
Link to post
Share on other sites

Super kurs/tutorial!🙂 jest to najlepszy cykl artykulow o tablicach jaki widzialem, a juz z kilkanascie przeczytalem...gratuluje i dziekuje bo tez nowych rzeczy sie dowiedzialem o ktorych inni nie wspominali👍👍👍

Edytowano przez farmaceuta
Link to post
Share on other sites
Dnia 3.09.2021 o 11:17, Gieneq napisał:

Napisy zapisujemy w apostrofach (np. "napis"), ale znaki w cudzysłowach (np. 'a').

Na pewno? 🙂

(przepraszam, dopiero teraz zauważyłem)

Link to post
Share on other sites
Zarejestruj się lub zaloguj, aby ukryć tę reklamę.
Zarejestruj się lub zaloguj, aby ukryć tę reklamę.

jlcpcb.jpg

jlcpcb.jpg

Produkcja i montaż PCB - wybierz sprawdzone PCBWay!
   • Darmowe płytki dla studentów i projektów non-profit
   • Tylko 5$ za 10 prototypów PCB w 24 godziny
   • Usługa projektowania PCB na zlecenie
   • Montaż PCB od 30$ + bezpłatna dostawa i szablony
   • Darmowe narzędzie do podglądu plików Gerber
Zobacz również » Film z fabryki PCBWay

@ethanak dziękuję, chciałem podkreślić tę różnicę i porządnie zamieszałem 😄 W innych wystąpieniach jest poprawnie.

@farmaceuta bardzo dziękuję, staram się podejść do tematu bardzo wnikliwie, choć zdaję sobie sprawę, że mało kto jest zainteresowany taką wnikliwością. Zaspojleruję, że jeszcze jedna część jest taka szczegółowa, później pojawiają się 2 przykłady. Szczególnie ostatni przykład może sporo wnieść - może nie w szczegółach implementacyjnych ale bardziej we wzorcach projektowych.

Tak w ramach offtopu, myślę ostatnio żeby napisać kilka artykułów o wzorcach projektowych używając Pythona, ale nad tym się jeszcze zastanowię 😉 

  • Lubię! 1
Link to post
Share on other sites

@Gieneq - to ja miałbym propozycję.

Wiem że o *printf można dużą i interesującą książkę napisać, ale po pierwsze nie warto kopiować tu podręcznika, a po drugie ostatnio mimo szczerych chęci z czasem u mnie trochę niespecjalnie...

Może warto dodać tutaj jakiś aneks dotyczący sprintf?

  • Po pierwsze: niebezpieczeństwo przepełnienia bufora
  • Po drugie: użycie bezpiecznego snprintf zamiast sprintf i sprawdzenie co zwraca
  • Po trzecie: sekwencje formatujące (przynajmniej te najważniejsze)
  • Po czwarte: co zrobić, aby *printf potrafił użyć floatów (dotyczy AVR-ów) i co w ogóle trzeba zrobić, aby funkcja printf mogła działać na AVR-owym Arduino.

Chyba warto, bo większość patrzy na rodzinę printf prawie jak na rodzinę Addamsów 😉

Coś w rodzaju (piszę z pamięci):

#define BUFSIZE 40
 
...
char buf[BUFSIZE];
if (snprintf(buf, BUFSIZE, "Napis: %s", jakis_napis) >= BUFSIZE) {
  snprintf(buf,BUFSIZE, "Napis: %-20.20s...", jakis_napis);
}

 

  • Lubię! 2
Link to post
Share on other sites

@ethanak dobry pomysł 🙂 gdzieś w połowie pisania kursu doszedłem do wniosku, że lepiej było zacząć pisać kurs C++ dla Arduino, bo wiele tematów jest potrzebnych, ale nie ma bezpośredniego związku z tematem tablic. Pomyślałem więc, że pójdę na kompromis (nie będę do końca zadowolny) - opiszę niektóre tematy pobieżnie, a jak coś wyniknie co będzie wymagać szerszego objaśnienia to zrobię dodatek. Także temat ten, wrzucę w dodatek, bo faktycznie jest przydatny. 👍

  • Lubię! 1
Link to post
Share on other sites

Dołącz do dyskusji, napisz odpowiedź!

Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!

Anonim
Dołącz do dyskusji! Kliknij i zacznij pisać...

×   Wklejony jako tekst z formatowaniem.   Przywróć formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.

×
×
  • 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.