Skocz do zawartości
Treker

Konwersja zmiennych integer na char/byte

Pomocna odpowiedź

Często zdarza się, że przez problemy z prostymi czynnościami nasze projekty zatrzymują się w miejscu na bardzo długo. W moim przypadku dzieje się tak zazwyczaj na etapie programowania mikrokontrolera. Dlatego postanowiłem rozpocząć serię krótkich wpisów z opisami częstych problemów początkujących.

UWAGA, to tylko wstęp! Dalsza część artykułu dostępna jest na blogu.

Przeczytaj całość »

Poniżej znajdują się komentarze powiązane z tym wpisem.

Udostępnij ten post


Link to post
Share on other sites

To chyba bardziej na szybkie pytania niż tutaj, bo z robotyką związek raczej słaby.

Kilka problemów:

1. Co robi w kodzie?

2. Opisana metoda jest lekko mówiąc daleka od optymalnej ( przydałoby się chociażby dodać inline )

3. Po co ktoś ma pisać

dane[0] = IntNaChar(kp,'L');
zamiast
dane[0] = kp & 0xFF;

W drugim przypadku mamy o wiele mniej znaków i jak ktoś obcy patrzy na nasz kod to nie musi się zastanawiać co robi funkcja IntNaChar.

Do tego zaraz dojdą liczby 32bitowe itd.

Sensowniej będzie napisać funkcję, której damy zmienną i wskaźnik na miejsce w tablicy znaków a ona od razu wpisze wszystkie bajty. Do tego można ją przeciążyć na różne typy (char, short, long) i masz jedną funkcję typu pr ( dane [ 0 ] , kp ) ; - dopiero takie coś ma sens.

Udostępnij ten post


Link to post
Share on other sites
To chyba bardziej na szybkie pytania niż tutaj, bo z robotyką związek raczej słaby.

Spotkałem taki problem (i nie tylko ja) pisząc komunikację z robotem rok temu, więc postanowiłem to opisać. Myślę, że lepiej publikować takie teksty, aby zmniejszać sukcesywnie ilość pytań o takie "nierobotyczne" problemy.

1) Tego już nie ma, jeśli widzisz to kwestia cache przeglądarki.

2), 3) Tak jak pisałem nie twierdzę, że jest to idealne rozwiązanie dla mnie było wystarczające,a używanie funkcji było łatwiejsze w praktyce. O wskaźnikach i tablicach nic tutaj nie dodawałem, bo jak widać tekst jest dla początkujących 🙂

Udostępnij ten post


Link to post
Share on other sites

Jeżeli mówimy o pisaniu programów przy użyciu kompilatora C/C++ na AVR (AVRGCC,) to moim zdaniem problem wywoływania funkcji z argumentami typu int8/int16 właściwie nie istnieje.

Żeby to wyjaśnić, trzeba sobie przypomnieć w jaki sposób ten kompilator przekazuje argumenty do wywoływanych funkcji. Aby możliwe było łączenie naszych programów np. z biblioteką napisaną na drugim końcu świata przez kogoś innego, muszą istnieć jakieś sztywne zasady których kompilator trzyma się podczas generacji kodu. Dzięki nim nasz program który wywołuje "obcą" funkcję zawsze "wie" jak to zrobić (tzn. gdzie załadować wartości odpowiednich argumentów) a wywoływana funkcja zawsze "wie" w których rejestrach dostaje to czego oczekuje. Takie zasady to jedna z podstaw konstrukcji kompilatora i oczywiście w przypadku GCC są one opisane w jego dokumentacji. Żeby nie zmuszać Was do przeszukiwania obszernych plików, pokrótce opiszę tutaj konwencję przyjętą w AVRGCC.

Jak zapewne wiecie, procesory AVR mają dość bogaty zestaw rejestrów wewnętrznych, numerowanych R0..R31. Dostęp do nich jest szybki, ponieważ są one ściśle powiązane z jednostką arytmetyczno-logiczną (ALU) procesora i aż się prosi o intensywne ich używanie (z czym z resztą kompilator świetnie sobie radzi). Jak łatwo się domyślić, to właśnie rejestry są głównym "kanałem przerzutowym" argumentów do funkcji. Oprócz rejestrów, AVRy zawierają też trochę RAMu. Większe więcej, mniejsze mniej - tutaj różnice są naprawdę spore. W RAMie przechowywane są oczywiście zmienne naszego programu, ale oprócz tego jest tam miejsce na stertę (heap) i na stos (stack). Stos - oprócz typowego zastosowania jakim jest pamiętanie adresów powrotów i chwilowe przechwowywanie zawartości rejestrów - jest "awaryjną" metodą przekazywania argumentów do funkcji. Dlaczego awaryjną? Zaraz się wyjaśni. A więc jest tak:

1. W celu przekazania argumentów do wywoływanej funkcji kompilator używa rejestrów R25-R8 w tej właśnie kolejności.

2. Rejestry "zużywane" są parami, tzn. na użytek nawet najprostszej funkcji jednorgumentowej np. mającej taką zapowiedź w pliku h:

void wypisz_znak(char ch);

kompilator i tak zarezerwuje parę R25:R24.

3. W przypadku dłuższych argumentów zużywane jest odpowiednio więcej rejestrów, tj. long/int32_t i float/double zabierają 4 rejestry (czyli 32 bity) a typ int64 aż 8 (64 bity). Wskaźniki w AVR są 16-bitowe więc mieszczą się w jednej parze.

4. W zależności od liczby argumentów funkcji, na potrzeby jej wywołania zawłaszczane są kolejne rejestry, ale zawsze z wyrównianiem do pary. Tak więc funkcja programująca np. timer 2:

void ustaw_pwm(uint8_t okres, uint8_t wypelnienie);

mimo, że wystarczyłyby dwa rejestry (2 * uint8_t) "zużyje" ich aż 4.

5. W przypadku wyczerpania puli rejestrów pozostałe argumenty kompilator przekazuje przez stos. Ta metoda jest dużo wolniejsza zarówno podczas wywoływania fukcji jak i podczas dostępu do argumentów w samej funkcji.

Czy coś to wyjaśnia/upraszcza? Moim zdaniem wiele.

Mając zmienną 16-bitową, którą chcemy wysłać przez UART i funkcję która wysyła znaki, zdefiniowane w taki sposób:

uint16_t licznik;
void putchar(char ch);

możemy zupełnie bezkarnie napisać:

putchar(licznik >> 8);
putchar(licznik);

i pierwsze wywołanie wyśle starszy bajt zmiennej a drugie - tylko młodszy.

Co więcej, operacja:

putchar(licznik & 0xFF);

nic nie daje. Operacja logiczna nie zmienia typu ani nie skraca długości. I bez ANDa kompilator załaduje i wyśle tylko młodszy bajt a jeśli nawet użyje pełnych 16 bitów to i tak to niczego nie zmienia -zgodnie z regułami musiał zarezerwować całą parę na argument funkcji a ta i tak wykorzysta tylko młodszy bajt.

Chcąc być w zgodzie z zasadami języka można użyć rzutowania po to, by zgadzały się typy argumentów: oczekiwanego przez funkcję i wysyłanego przez program:

putchar((char)(licznik >> 8));
putchar((char)licznik);

ale to także nie może zmienić zasad przekazywania argumentów: w obu przypadkach na potrzeby tego wywołania zostanie zarezerwowana para rejestrów i następne argumenty (gdyby były) używałyby rejestrów od R23 w górę.

Przez analogię wysyłanie dłuższych typów możemy zrobić następująco:

uint32_t suma;
putchar((char)(suma >> 24));
putchar((char)(suma >> 16));
putchar((char)(suma >> 8));
putchar((char)(suma >> 0));

Jeżeli system optymalizacji się połapie, każde wywołanie funkcji będzie poprzedzone pobraniem z pamięci tylko jednego, od razu prawidłowego bajtu zmiennej suma. Jeżeli optymalizacje będą wyłączone, za każdym razem dostaniemy 4 rozkazy pobrania 32 bitów z pamięci oraz kilka operacji przesuwania po to, by za każdym razem w R24 znalazł się żądany bajt. W pośrednim przypadku kompilator przepisuje całe bajty widząc, że przesuwania są wielokrotnościami 8 bitów.

Jeśli ktoś jest ciekaw jak kompilator radzi sobie w różnych nieoczywistych sytuacjach, proponuję oglądanie listingów asemblerowych kody wynikowego. Naprawdę wielu ciekawych rzeczy można się dowiedzieć i zrozumieć dlaczego niektóre nasze "wyczyny" przekładają się na jasny i szybki kod binarny a inne męczą procesor i pamięć mnóstwem nadmiarowych rozkazów.

Oczywiście wszystko powyższe nie dotyczy BASCOMów itp wynalazków.

Udostępnij ten post


Link to post
Share on other sites

Jeżeli chodzi o podział i scalanie liczb

dla mnie najlepszym rozwiązaniem

jest deklaracja uni i struktury.

Jest ona bardziej uniwersalna.

Struktura jest "sklejeniem" zmiennych

czyli deklarując 4 zmienne są w pamięci złożone jak wektor

ale każda zmienna może być innego typu

a rozmiar struktury jest sumą rozmiarów wszystkich zmiennych w niej zawartych.

Unia jest "złożeniem" zmiennych

w tym przypadku rozmiar unii jest równy rozmiarowi największej zawartej w niej zmiennej.

Dawno nie pisałem w C ale jak będą zainteresowani to mogę odgrzebać jakiś

stary kod i napisać krótki artykulik.

(jakbym przez dłuższy czas nie odpisywał to proszę o kontakt i przypomnienie

na priw ostatnio niestety rzadko goszczę na forboce)

Pozdrawiam

Zuk

Udostępnij ten post


Link to post
Share on other sites

Można jeszcze wysyłać duże liczby w postaci kodów ASCII. Przykładowo liczbę 5357 można wysłać jako 4 bajty (znaki).

Cyfra 5 to 0x30 (czyli zero) + 5 co daję 0x35 czyli kod cyfry 5 w kodzie ASCII

Cyfra 3 to 0x30 + 3 co daję 0x33 itp.

Dekodowanie tego też jest bardzo proste, bo wystarczy od odebranego bajtu odjąć zero czyli 0x30 i mamy już interesującą nas cyfrę, a z cyfr można już uzyskać liczbę.

odebrana_liczba = pierwsza_cyfra*10000 + druga_cyfra*1000 + trzecia_cyfra*10 + czwarta_cyfra

Nie jest to może jakoś super optymalne, ale zawsze jakiś inny sposób wysyłania danych.

Udostępnij ten post


Link to post
Share on other sites

Jestem nowy na forum więc witam wszystkich 🙂

Zaciekawił mnie ten artykuł --> https://forbot.pl/blog/konwersja-zmiennych-integer-charbyte-id2860 i w związku z nim mam pytanie.

Czy w miejscu zaznaczonym czerwoną ramką nie ma błędu? Przesunięcie bitowe o 8 znaków w prawo miało wyciągnąć starszy bajt, ale autor napisał cyt. "Pamiętajmy, że zera z przodu nic nie znaczą, więc tak naprawdę otrzymaliśmy liczbę 8 bitową:" i podał młodszy bajt 1 1 1 0 1 1 0 1.

Czy nie powinien w tym miejscu podać starszego bajtu 0 0 0 1 0 1 0 0 ?

Poniżej zauważyłem też, że autor napisał o przesunięciu w prawo, a przesunął w lewo. Czy jest to pomyłka?

  • Pomogłeś! 1

Udostępnij ten post


Link to post
Share on other sites

1. Co do pierwszego punktu to nie masz racji. Skoro mamy zmienną tylko 16 bitową, to przesunięcie o 8 bitów w prawo spowoduje, że starszy bajt znajdzie się na miejscu młodszego, a w jego miejsce wpiszemy same zera - dokładnie tak jak to przedstawił autor. (Co innego gdybyśmy mieli wartość 32bitową i przesunęli tylko o 8 bitów).

2. Tutaj masz rację - autor mówi o przesunięciu w prawo, a przesuwa w lewo...

Udostępnij ten post


Link to post
Share on other sites

Fort, masz rację w obu przypadkach - gratuluję spostrzegawczości. Autor na pewno poprawi błędy w wolnej chwili. Nie myli się podobno tylko ten kto nic nie robi 🙂

Udostępnij ten post


Link to post
Share on other sites
1. Co do pierwszego punktu to nie masz racji. Skoro mamy zmienną tylko 16 bitową, to przesunięcie o 8 bitów w prawo spowoduje, że starszy bajt znajdzie się na miejscu młodszego, a w jego miejsce wpiszemy same zera - dokładnie tak jak to przedstawił autor.

Przesunięcie jest ok, ale spójrz, że w zaznaczonej na czerwono ramce zamiast starszego bajtu wpisał młodszy bajt.

Udostępnij ten post


Link to post
Share on other sites

Fort, witam na forum i dziękuję za zgłoszenie uwag 🙂 Tak jak koledzy wyżej napisali niedługo zajmę się tym tematem i wprowadzę stosowne poprawki.

Aktualizacja: jeszcze raz dziękuję za zgłoszenia - wszystko zostało poprawione.

Udostępnij ten post


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!

Gość
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...