Dlaczego printf () jest zły do ​​debugowania systemów wbudowanych?

16

Myślę, że źle jest próbować debugować projekt oparty na mikrokontrolerze printf().

Rozumiem, że nie masz predefiniowanego miejsca na wyjście i że może zużywać cenne szpilki. Jednocześnie widziałem, jak ludzie używają pinu UART TX do wysyłania do terminala IDE za pomocą niestandardowego DEBUG_PRINT()makra.

tarabajt
źródło
12
Kto ci powiedział, że jest źle? „Zwykle nie najlepszy” to nie to samo, co „niekwalifikowany” zły.
Spehro Pefhany
6
Cała ta rozmowa mówi o tym, ile jest narzutu, jeśli wszystko, co musisz zrobić, to wyprowadzić komunikaty „Jestem tutaj”, wcale nie potrzebujesz printf, tylko procedura wysyłania ciągu do UART. To plus kod inicjujący UART prawdopodobnie ma mniej niż 100 bajtów kodu. Dodanie możliwości generowania kilku wartości szesnastkowych nie zwiększy tego aż tak bardzo.
tcrosley
7
@ChetanBhargava - Pliki nagłówkowe C zwykle nie dodają kodu do pliku wykonywalnego. Zawierają deklaracje; jeśli reszta kodu nie używa deklarowanych rzeczy, kod tych rzeczy nie zostaje połączony. Jeśli użyjesz printf, oczywiście, cały kod potrzebny do implementacji printfzostanie połączony z plikiem wykonywalnym. Ale to dlatego, że kod go używał, a nie z powodu nagłówka.
Pete Becker
2
@ChetanBhargava Nie musisz nawet dołączać <stdio.h>, jeśli rzucisz własną prostą procedurą, aby wyprowadzić ciąg znaków, jak opisano (
wypisz
2
@ tcrosley Myślę, że ta rada jest prawdopodobnie dyskusyjna, jeśli masz dobry nowoczesny kompilator, jeśli używasz printf w prostym przypadku bez ciągów formatu gcc, a większość innych zastępuje go bardziej wydajnym prostym wywołaniem, które działa tak, jak opisano.
Rzeczywistość

Odpowiedzi:

24

Mogę wymyślić kilka wad korzystania z printf (). Należy pamiętać, że „system osadzony” może wahać się od czegoś z kilkoma setkami bajtów pamięci programu do w pełni wyposażonego systemu QNX RTOS montowanego w szafie rack z gigabajtami pamięci RAM i terabajtami pamięci nieulotnej.

  • Do przesłania danych potrzebne jest miejsce. Być może masz już w systemie port debugowania lub programowania, a może nie. Jeśli nie (lub ten, który masz, nie działa), nie jest to bardzo przydatne.

  • To nie jest lekka funkcja we wszystkich kontekstach. To może być wielka sprawa, jeśli masz mikrokontroler z zaledwie kilkoma K pamięci, ponieważ łączenie w printf może samo z siebie pochłonąć 4K. Jeśli masz mikrokontroler 32 K lub 256 K, prawdopodobnie nie jest to problem, nie mówiąc już o dużym systemie osadzonym.

  • Jest mało lub nie ma sensu znajdowanie pewnych problemów związanych z alokacją pamięci lub przerwaniami i może zmienić zachowanie programu, gdy instrukcje są dołączone lub nie.

  • Jest to dość bezużyteczne w przypadku rzeczy wrażliwych na czas. Lepiej byłoby z analizatorem logicznym i oscyloskopem lub analizatorem protokołów, a nawet symulatorem.

  • Jeśli masz duży program i musisz wielokrotnie kompilować, zmieniając instrukcje printf i zmieniając je, możesz stracić dużo czasu.

Po co jest dobry - to szybki sposób na wyprowadzenie danych w wstępnie sformatowany sposób, który każdy programista C wie, jak korzystać - zero krzywej uczenia się. Jeśli chcesz wyrzucić matrycę dla filtru Kalmana, który debugujesz, może być miło wypluć ją w formacie, w którym MATLAB mógłby ją odczytać. Z pewnością lepiej niż przeglądać lokalizacje pamięci RAM pojedynczo w debugerze lub emulatorze .

Nie sądzę, aby była to bezużyteczna strzała w kołczanie, ale powinna być używana oszczędnie, wraz z gdb lub innymi debuggerami, emulatorami, analizatorami logicznymi, oscyloskopami, narzędziami do analizy kodu statycznego, narzędziami do pokrywania kodu i tak dalej.

Spehro Pefhany
źródło
3
Większość printf()implementacji nie jest bezpieczna dla wątków (tj. Nie wymaga ponownej rejestracji), co nie jest zabójcą transakcji, ale jest czymś, o czym należy pamiętać, używając jej w środowisku wielowątkowym.
JRobert
1
@JRobert porusza dobrą sprawę .. i nawet w środowisku bez systemu operacyjnego trudno jest wykonać bardzo użyteczne bezpośrednie debugowanie ISR. Oczywiście, jeśli wykonujesz matematykę printf () lub zmiennoprzecinkową w ISR, podejście prawdopodobnie jest wyłączone.
Spehro Pefhany
@JRobert Jakie narzędzia do debugowania mają twórcy oprogramowania pracujący w środowisku wielowątkowym (w ustawieniach sprzętowych, w których użycie analizatorów logicznych i oscyloskopów nie jest praktyczne)?
Minh Tran
1
W przeszłości stworzyłem własny bezpieczny dla wątków printf (); używane ekwiwalenty boso puts () lub putchar () do wypluwania bardzo zwięzłych danych do terminala; przechowywane dane binarne w tablicy, którą zrzuciłem i zinterpretowałem po uruchomieniu testowym; użył portu I / O do mrugania diody LED lub generowania impulsów w celu wykonania pomiarów czasowych za pomocą oscyloskopu; wypluj liczbę do D / A i zmierzoną za pomocą VOM ... Lista jest tak długa, jak twoja wyobraźnia i odwrotnie tak duża, jak twój budżet! :)
JRobert
19

Oprócz niektórych innych dokładnych odpowiedzi, wysyłanie danych do portu z szeregowymi prędkościami transmisji może być wręcz powolne w stosunku do czasu pętli i mieć wpływ na sposób, w jaki pozostała część programu działa (podobnie jak DOWOLNY debugowanie proces).

Jak mówili inni ludzie, nie ma nic „złego” w używaniu tej techniki, ale ma ona, podobnie jak wiele innych technik debugowania, swoje ograniczenia. Tak długo, jak znasz i potrafisz sobie poradzić z tymi ograniczeniami, może być to bardzo dogodna okazja, aby pomóc ci poprawić kod.

Systemy wbudowane mają pewien stopień nieprzezroczystości, co generalnie sprawia, że ​​debugowanie jest trochę problemem.

Scott Seidman
źródło
8
+1 dla „systemów wbudowanych ma pewien stopień krycia”. Chociaż obawiam się, że to stwierdzenie może być zrozumiałe tylko dla tych, którzy mają przyzwoite doświadczenie w pracy z osadzonymi, stanowi to miłe, zwięzłe podsumowanie sytuacji. W rzeczywistości jest zbliżona do definicji „osadzonego”.
njahnke
5

Istnieją dwa główne problemy, na które napotkasz podczas próby użycia printfna mikrokontrolerze.

Po pierwsze, może być problem z umieszczeniem wyjścia w odpowiednim porcie. Nie zawsze. Ale niektóre platformy są trudniejsze niż inne. Niektóre pliki konfiguracyjne mogą być słabo udokumentowane i konieczne może być wiele eksperymentów.

Drugi to pamięć. Pełna printfbiblioteka może być DUŻA. Czasami nie potrzebujesz wszystkich specyfikatorów formatu i mogą być dostępne specjalne wersje. Na przykład stdio.h dostarczone przez AVR zawiera trzy różne printfrozmiary i funkcje o różnych rozmiarach.

Ponieważ pełne wdrożenie wszystkich wymienionych funkcji staje się dość duże, vfprintf()można wybrać trzy różne smaki za pomocą opcji linkera. Domyślnie vfprintf()implementuje wszystkie wymienione funkcje oprócz konwersji zmiennoprzecinkowych. Dostępna jest zminimalizowana wersja, vfprintf()która implementuje tylko bardzo podstawowe funkcje konwersji liczb całkowitych i ciągów, ale tylko #dodatkową opcję można określić za pomocą flag konwersji (flagi te są poprawnie analizowane ze specyfikacji formatu, a następnie po prostu ignorowane).

Miałem instancję, w której nie było dostępnej biblioteki i miałem minimalną pamięć. Więc nie miałem wyboru, jak użyć niestandardowego makra. Ale użycie printflub nie jest tak naprawdę jednym z tych, które pasują do twoich wymagań.

embedded.kyle
źródło
Czy downvoter mógłby wyjaśnić, co w mojej odpowiedzi jest niepoprawne, abym mógł uniknąć mojego błędu w przyszłych projektach?
embedded.kyle
4

Aby dodać do tego, co Spehro Pefhany mówił o „rzeczach wrażliwych na czas”: weźmy przykład. Załóżmy, że masz żyroskop, z którego wbudowany system wykonuje 1000 pomiarów na sekundę. Chcesz debugować te pomiary, więc musisz je wydrukować. Problem: wydrukowanie ich powoduje, że system jest zbyt zajęty, aby odczytać 1000 pomiarów na sekundę, co powoduje przepełnienie bufora żyroskopu, co powoduje odczyt (i wydruk) uszkodzonych danych. I tak, drukując dane, uszkodziłeś dane, co sprawia, że ​​myślisz, że istnieje błąd w odczycie danych, a może nie. Tak zwany heisenbug.

njahnke
źródło
lol! Czy „heisenbug” jest naprawdę terminem technicznym? Myślę, że ma to związek z pomiarem stanu cząstek i zasadą Heisenburga ...
Zeta.Investigator,
3

Głównym powodem braku debugowania za pomocą printf () jest to, że jest on zwykle nieefektywny, nieodpowiedni i niepotrzebny.

Nieefektywne: printf () i kin używają dużo pamięci flash i pamięci RAM w stosunku do tego, co jest dostępne na małym mikrokontrolerze, ale większa nieefektywność polega na rzeczywistym debugowaniu. Zmiana rejestrowanych danych wymaga ponownej kompilacji i przeprogramowania celu, co spowalnia proces. Wykorzystuje również UART, którego można by użyć do wykonywania użytecznej pracy.

Nieodpowiednie: Istnieje tylko tyle szczegółów, które można przesłać przez łącze szeregowe. Jeśli program się zawiesi, nie wiesz dokładnie gdzie, tylko ostatnie zakończone wyjście.

Niepotrzebne: wiele mikrokontrolerów można zdalnie debugować. JTAG lub zastrzeżone protokoły mogą być używane do zatrzymywania procesora, podglądania rejestrów i pamięci RAM, a nawet zmiany stanu działającego procesora bez konieczności ponownej kompilacji. Dlatego debuggery są ogólnie lepszym sposobem debugowania niż instrukcje drukowania, nawet na komputerze PC z dużą ilością miejsca i mocy.

Szkoda, że ​​najpopularniejsza platforma mikrokontrolera dla początkujących, Arduino, nie ma debugera. AVR obsługuje zdalne debugowanie, ale protokół debugWIRE Atmela jest zastrzeżony i nieudokumentowany. Możesz użyć oficjalnej tablicy deweloperów do debugowania z GDB, ale jeśli tak, prawdopodobnie nie martwisz się już Arduino.

Theran
źródło
Czy nie możesz użyć wskaźników funkcji do zabawy z tym, co jest rejestrowane, i dodać całą elastyczność?
Scott Seidman
3

printf () nie działa samodzielnie. Wywołuje wiele innych funkcji, a jeśli masz mało miejsca na stosie, możesz go wcale nie użyć do debugowania problemów zbliżonych do limitu stosu. W zależności od kompilatora i mikrokontrolera ciąg formatu może również zostać umieszczony w pamięci, a nie odwołany z pamięci flash. Można to znacznie zsumować, jeśli wkleisz kod za pomocą instrukcji printf. Jest to duży problem w środowisku Arduino - początkujący używający dziesiątek lub setek instrukcji printf nagle wpadają w pozornie przypadkowe problemy, ponieważ nadpisują swoje stosy stosem.

Adam Davis
źródło
2
Doceniam informacje zwrotne, które przekazuje sama opinia, ale byłoby bardziej pomocne dla mnie i innych, gdyby ci, którzy się nie zgadzają, wyjaśnili problemy z tą odpowiedzią. Wszyscy jesteśmy tutaj, aby uczyć się i dzielić wiedzą, rozważ dzielenie się swoją wiedzą.
Adam Davis,
3

Nawet jeśli ktoś chce wyrzucić dane na jakąś konsolę rejestrującą, printffunkcja na ogół nie jest zbyt dobrym sposobem na zrobienie tego, ponieważ musi zbadać przekazany ciąg formatu i parsować go w czasie wykonywania; nawet jeśli kod nigdy nie używa żadnego specyfikatora formatu innego niż %04X, kontroler zazwyczaj będzie musiał zawrzeć cały kod, który byłby wymagany do parsowania dowolnych ciągów formatu. W zależności od używanego kontrolera użycie kodu może być o wiele bardziej wydajne:

void log_string(const char *st)
{
  int ch;
  do
  {
    ch = *st++;
    if (ch==0) break;
    log_char(ch);
  } while(1);
}
void log_hexdigit(unsigned char d)
{
  d&=15;
  if (d > 9) d+=7;
  log_char(d+'0');
}
void log_hexbyte(unsigned char b)
{ log_hexdigit(b >> 4); log_hexdigit(b); }
void log_hexi16(uint16_t s)
{ log_hexbyte(s >> 8); log_hexbyte(s); }
void log_hexi32(uint32_t i)
{ log_hexbyte(i >> 24); log_hexbyte(i >> 16); log_hexbyte(i >> 8); log_hexbyte(i); }
void log_hexi32p(uint32_t *p) // On a platform where pointers are less than 32 bits
{ log_hexi32(*p); }

Na niektórych mikrokontrolerach PIC log_hexi32(l)prawdopodobnie wziąłby 9 instrukcji i mógłby zająć 17 (jeśli lznajduje się w drugim banku), podczas gdy log_hexi32p(&l)zająłby 2. Sama log_hexi32pfunkcja mogłaby być napisana na około 14 instrukcji, więc zwróciłaby się za dwukrotne wywołanie .

supercat
źródło
2

Jeden punkt, o którym żadna z pozostałych odpowiedzi nie wspomniała: W podstawowej mikro (IE jest tylko pętla główna () i być może kilka ISR działa w dowolnym momencie, a nie wielowątkowy system operacyjny), jeśli zawiesza się / zatrzymuje / dostaje utknąwszy w pętli, funkcja drukowania po prostu się nie wydarzy .

Ponadto ludzie powiedzieli „nie używaj printf” lub „stdio.h zajmuje dużo miejsca”, ale nie dali wiele alternatywy - embedded.kyle wspomina o uproszczonych alternatywach i jest to dokładnie to, czym prawdopodobnie powinieneś być robi to oczywiście na podstawowym systemie wbudowanym. Podstawową procedurą wypychania kilku znaków z UART może być kilka bajtów kodu.

John U
źródło
Jeśli twój printf się nie zdarzy, nauczyłeś się dużo o tym, gdzie twój kod jest problematyczny.
Scott Seidman
Zakładając, że masz tylko jeden printf, który może się zdarzyć, tak. Ale przerwania mogą być uruchamiane setki razy w czasie, który wymaga wywołania printf (), aby wydostać się z UART
John U