Jak zrobić TDD na urządzeniach wbudowanych?

17

Nie jestem nowy w programowaniu i nawet pracowałem z jakimś niskim poziomem C i ASM na AVR, ale tak naprawdę nie mogę się skupić na projekcie C osadzonym na większą skalę.

Zdegenerowana przez filozofię Ruby TDD / BDD, nie jestem w stanie zrozumieć, jak ludzie piszą i testują kod w ten sposób. Nie twierdzę, że to zły kod, po prostu nie rozumiem, jak to może działać.

Chciałem bardziej zaangażować się w programowanie na niskim poziomie, ale tak naprawdę nie mam pojęcia, jak do tego podejść, ponieważ wygląda to na zupełnie inny sposób myślenia, do którego jestem przyzwyczajony. Nie mam problemów ze zrozumieniem arytmetyki wskaźników ani sposobu przydzielania pamięci, ale kiedy widzę, jak skomplikowany kod C / C ++ wygląda w porównaniu do Ruby, wydaje się to po prostu niemożliwe.

Ponieważ już zamówiłem sobie tablicę Arduino, chciałbym uzyskać więcej informacji na temat niskiego poziomu C i naprawdę rozumiem, jak robić to poprawnie, ale wydaje się, że nie obowiązują żadne zasady dotyczące języków wysokiego poziomu.

Czy możliwe jest nawet wykonywanie TDD na urządzeniach wbudowanych lub podczas opracowywania sterowników lub innych niestandardowych programów ładujących itp.?

Jakub Arnold
źródło
3
Cześć Darth, naprawdę nie możemy pomóc ci przezwyciężyć lęku przed C, ale pytanie o TDD na urządzeniach wbudowanych jest tutaj na ten temat: Zmieniłem twoje pytanie, aby zamiast tego włączyć.

Odpowiedzi:

18

Po pierwsze, powinieneś wiedzieć, że próba zrozumienia kodu, którego nie napisałeś, jest 5 razy trudniejsza niż samodzielne napisanie go. Możesz nauczyć się C czytając kod produkcyjny, ale zajmie to znacznie więcej czasu niż uczenie się przez działanie.

Zdegenerowana przez filozofię Ruby TDD / BDD, nie jestem w stanie zrozumieć, jak ludzie piszą i testują kod w ten sposób. Nie twierdzę, że to zły kod, po prostu nie rozumiem, jak to może działać.

To umiejętność; stajesz się w tym lepszy. Większość programistów C nie rozumie, jak ludzie używają Ruby, ale to nie znaczy, że nie mogą.

Czy możliwe jest nawet wykonywanie TDD na urządzeniach wbudowanych lub podczas opracowywania sterowników lub innych niestandardowych programów ładujących itp.?

Cóż, są książki na ten temat:

wprowadź opis zdjęcia tutaj Jeśli trzmiel może to zrobić, Ty też możesz!

Pamiętaj, że stosowanie praktyk z innych języków zwykle nie działa. TDD jest jednak dość uniwersalny.

Pubby
źródło
2
Każde TDD, które widziałem dla moich systemów wbudowanych, znajdowało tylko błędy w systemach, które miały łatwe do rozwiązania błędy, które z łatwością znalazłem sam. Nigdy nie znaleźliby tego, czego potrzebuję pomocy, interakcji zależnych od czasu z innymi chipami i interakcji przerywających.
Kortuk
3
Zależy to od rodzaju systemu, nad którym pracujesz. Przekonałem się, że używanie TDD do testowania oprogramowania w połączeniu z dobrą abstrakcją sprzętową pozwala mi znacznie łatwiej naśladować interakcje zależne od czasu. Inną korzyścią, na którą ludzie często patrzą, jest to, że zautomatyzowane testy można uruchomić w dowolnym momencie i nie wymagają one, aby ktoś usiadł na urządzeniu z analizatorem logicznym, aby upewnić się, że oprogramowanie działa. TDD zaoszczędził mi tygodni debugowania samego mojego obecnego projektu. Często błędy, które naszym zdaniem są łatwe do wykrycia, powodują błędy, których się nie spodziewamy.
Nick Pascucci,
Ponadto umożliwia tworzenie i testowanie poza celem.
cp.engr
Czy mogę postępować zgodnie z tą książką, aby zrozumieć TDD dla niewbudowanego C? Do programowania w przestrzeni użytkownika C?
nadmierna wymiana
15

Duża różnorodność odpowiedzi tutaj ... głównie na różne sposoby.

Od ponad 25 lat piszę wbudowane oprogramowanie niskopoziomowe i oprogramowanie układowe w różnych językach - głównie C (ale z różnymi wersjami Ady, Occam2, PL / M i różnych asemblerów po drodze).

Po długim okresie refleksji i prób i błędów zdecydowałem się na metodę, która dość szybko uzyskuje wyniki i dość łatwo jest tworzyć opakowania testowe i uprzęże (w których DODAJĄ WARTOŚĆ!)

Metoda przebiega mniej więcej tak:

  1. Napisz sterownik lub jednostkę kodu abstrakcji sprzętu dla każdego głównego urządzenia peryferyjnego, którego chcesz użyć. Napisz też jeden z nich, aby zainicjować procesor i skonfigurować wszystko (to tworzy przyjazne środowisko). Zwykle na małych procesorach osadzonych - na przykład twój AVR - może być 10-20 takich jednostek, wszystkie małe. Mogą to być jednostki inicjujące, konwersja A / D do nieskalowanych buforów pamięci, wyjście bitowe, wejście przycisku (bez próbkowania tylko próbkowania), sterowniki modulacji szerokości impulsu, proste sterowniki szeregowe UART / przerwania użycia i małe bufory we / wy. Może być jeszcze kilka innych - np. Sterowniki I2C lub SPI dla EEPROM, EPROM lub innych urządzeń I2C / SPI.

  2. Następnie dla każdej jednostki abstrakcji sprzętu (HAL) / sterownika piszę program testowy. Zależy to od portu szeregowego (UART) i inicjacji procesora - więc pierwszy program testowy wykorzystuje tylko te 2 jednostki i po prostu wykonuje podstawowe operacje wejścia i wyjścia. To pozwala mi przetestować, czy mogę uruchomić procesor i czy mam podstawową obsługę szeregowego we / wy debugowania. Kiedy to zadziała (i tylko wtedy), czy opracowuję inne programy testowe HAL, budując je na znanych urządzeniach UART i INIT. Mogę więc mieć programy testowe do odczytu danych bitowych i wyświetlania ich w ładnej formie (szesnastkowa, dziesiętna, cokolwiek) na moim terminalu szeregowego debugowania. Następnie mogę przejść do większych i bardziej złożonych rzeczy, takich jak programy testowe EEPROM lub EPROM - większość tych menu steruję, więc mogę wybrać test do uruchomienia, uruchomić go i zobaczyć wynik. Nie mogę tego PRZESŁAĆ, ale zwykle nie

  3. Po uruchomieniu całego HAL znajduję sposób na uzyskanie regularnego tykania timera. Zazwyczaj jest to z częstotliwością pomiędzy 4 a 20 ms. To musi być regularne, generowane w przerwie. Rolowanie / przepełnienie liczników jest zwykle sposobem, w jaki można to zrobić. Program obsługi przerwań INCREMENTS następnie „semafor” o rozmiarze bajtu. W tym momencie możesz także majstrować przy zarządzaniu energią, jeśli potrzebujesz. Idea semafora polega na tym, że jeśli jego wartość wynosi> 0, musisz uruchomić „główną pętlę”.

  4. EXECUTIVE uruchamia główną pętlę. Prawie tylko czeka na tym semaforze, aby stał się niezerowy (odciągam ten szczegół od siebie). W tym momencie możesz bawić się licznikami, aby policzyć te tiki (ponieważ znasz częstotliwość tykania), dzięki czemu możesz ustawić flagi pokazujące, czy bieżący tik wykonawczy trwa przez 1 sekundę, 1 minutę i inne typowe interwały może chcieć użyć. Gdy dyrektor wykonawczy wie, że semafor ma wartość> 0, uruchamia pojedyncze przejście przez każdą funkcję „aktualizacji” procesów „aplikacji”.

  5. Procesy aplikacyjne skutecznie siedzą obok siebie i są uruchamiane regularnie przez zaznaczenie „aktualizacja”. Jest to tylko funkcja wywoływana przez organ wykonawczy. Jest to efektywne, wielozadaniowe narzędzie z bardzo prostym, domowym systemem RTOS, który polega na wchodzeniu do wszystkich aplikacji, pracy i wyjściu. Aplikacje muszą utrzymywać własne zmienne stanu i nie mogą wykonywać długotrwałych obliczeń, ponieważ nie ma wyprzedzającego systemu operacyjnego, który wymusiłby uczciwość. Oczywiście czas działania aplikacji (narastająco) powinien być mniejszy niż główny okres zaznaczania.

Powyższe podejście można łatwo rozszerzyć, dzięki czemu można dodawać takie stosy komunikacyjne, które działają asynchronicznie, a komunikaty komunikacyjne mogą być następnie dostarczane do aplikacji (dodajesz nową funkcję do każdej z nich, która jest „modułem obsługi rx_message” i piszesz dyspozytora komunikatów, który przedstawia do której aplikacji wysłać).

To podejście działa na prawie każdym systemie komunikacyjnym, który chcesz nazwać - może (i wykonało) pracę na wielu zastrzeżonych systemach, systemach komunikacji z otwartymi standardami, a nawet na stosach TCP / IP.

Ma również tę zaletę, że składa się z elementów modułowych z dobrze zdefiniowanymi interfejsami. W każdej chwili możesz wciągać i wyjmować elementy, zamieniać różne elementy. W każdym punkcie po drodze możesz dodać uprząż testową lub uchwyty, które opierają się na znanych dobrych częściach dolnej warstwy (poniżej). Odkryłem, że około 30% do 50% projektu może zyskać na dodaniu specjalnie napisanych testów jednostkowych, które zazwyczaj są dość łatwe do dodania.

Zrobiłem to o krok dalej (pomysł, który podrobiłem od kogoś, kto to zrobił) i zastąpiłem warstwę HAL odpowiednikiem na PC. Na przykład możesz używać C / C ++ i winforms lub podobnych na komputerze PC i PORZĄDKU pisząc kod, możesz emulować każdy interfejs (np. EEPROM = plik dyskowy wczytany do pamięci komputera), a następnie uruchomić całą wbudowaną aplikację na komputerze. Możliwość korzystania z przyjaznego środowiska debugowania pozwala zaoszczędzić dużo czasu i wysiłku. Tylko naprawdę duże projekty zazwyczaj uzasadniają taki wysiłek.

Powyższy opis jest czymś, co nie jest unikalne w tym, jak robię rzeczy na platformach osadzonych - natknąłem się na wiele organizacji komercyjnych, które robią podobnie. Sposób jego wykonania jest zwykle bardzo różny we wdrażaniu, ale zasady są często takie same.

Mam nadzieję, że powyższe daje trochę smaku ... to podejście działa w przypadku małych systemów wbudowanych, które działają w kilku kilobajtach z agresywnym zarządzaniem baterią, aż do potworów o mocy 100 000 lub więcej linii zasilanych na stałe. Jeśli uruchamiasz „osadzony” na dużym systemie operacyjnym, takim jak Windows CE itp., Wszystkie powyższe elementy są całkowicie nieistotne. Ale tak czy inaczej, to nie jest PRAWDZIWE oprogramowanie wbudowane.

szybko. teraz
źródło
2
Większość sprzętowych urządzeń peryferyjnych, których nie można przetestować za pomocą UART, ponieważ dość często interesuje Cię przede wszystkim charakterystyka czasowa. Jeśli chcesz sprawdzić częstotliwość próbkowania ADC, cykl pracy PWM, zachowanie niektórych innych szeregowych urządzeń peryferyjnych (SPI, CAN itp.) Lub po prostu czas wykonania części programu, nie możesz tego zrobić poprzez UART. Wszelkie poważne testy wbudowanego oprogramowania układowego obejmują oscyloskop - bez niego nie można programować systemów wbudowanych.
1
O tak, absolutnie. Po prostu zapomniałem o tym wspomnieć. Ale kiedy już uruchomisz UART, bardzo łatwo jest skonfigurować przypadki testowe lub testowe (o to chodziło w pytaniu), stymulować, pozwalać na wkładanie danych, uzyskiwać wyniki i wyświetlać w przyjazny sposób. To + twoje CRO sprawia, że ​​życie jest bardzo łatwe.
szybko_now
2

Kod, który ma długą historię stopniowego rozwoju i optymalizacji dla wielu platform, takich jak wybrane przez Ciebie przykłady, jest zwykle trudniejszy do odczytania.

Chodzi o to, że C jest w stanie obejmować platformy w szerokim zakresie bogactwa interfejsu API i wydajności sprzętu (i jego braku). MacVim działał responsywnie na maszynach z ponad 1000-krotnie mniejszą pamięcią i wydajnością procesora niż typowy smartfon dzisiaj. Czy Twój kod Ruby? To jeden z powodów, dla których może wyglądać to prostiej niż wybrane przez Ciebie dojrzałe przykłady C.

hotpaw2
źródło
2

Jestem w odwrotnej sytuacji - spędziłem większość ostatnich 9 lat jako programista C, a ostatnio pracowałem nad niektórymi nakładkami Ruby on Rails.

Rzeczy, nad którymi pracuję w C, to w większości średniej wielkości niestandardowe systemy do kontrolowania automatycznych magazynów (typowy koszt to kilkaset tysięcy funtów, do kilku milionów). Przykładowa funkcjonalność to niestandardowa baza danych w pamięci, łącząca się z maszynami z krótkimi wymaganiami dotyczącymi czasu reakcji i wyższym poziomem zarządzania przepływem pracy magazynu.

Po pierwsze mogę powiedzieć, że nie robimy TDD. Wielokrotnie próbowałem wprowadzić testy jednostkowe, ale w C to więcej kłopotów niż jest to warte - przynajmniej przy tworzeniu niestandardowego oprogramowania. Ale powiedziałbym, że TDD jest znacznie mniej potrzebny w C niż Ruby. Głównie dlatego, że C jest kompilowany, a jeśli kompiluje się bez ostrzeżeń, wykonałeś już dość podobną liczbę testów do testów rusztowań generowanych automatycznie przez rspec w Railsach. Ruby bez testów jednostkowych jest niewykonalny.

Ale powiedziałbym, że C nie musi być tak trudne, jak niektórzy to robią. Znaczna część standardowej biblioteki C to bałagan niezrozumiałych nazw funkcji, a wiele programów C przestrzega tej konwencji. Z przyjemnością stwierdzam, że nie, i faktycznie mamy wiele opakowań dla standardowej funkcjonalności biblioteki (ST_Copy zamiast strncpy, ST_PatternMatch zamiast regcomp / regexec, CHARSET_Convert zamiast iconv_open / iconv / iconv_close i tak dalej). Nasz wewnętrzny kod C czyta mi się lepiej niż większość innych rzeczy, które przeczytałem.

Ale kiedy mówisz, że reguły z innych języków wyższego poziomu wydają się nie mieć zastosowania, nie zgadzam się. Wiele dobrego kodu C „wydaje się” zorientowane obiektowo. Często widzisz wzorzec inicjowania dojścia do zasobu, wywoływania niektórych funkcji przekazujących uchwyt jako argument i ostatecznie uwalniającego zasób. Rzeczywiście zasady projektowania programowania obiektowego w dużej mierze wynikały z dobrych rzeczy, które ludzie robili w językach proceduralnych.

Czasy, w których C naprawdę się komplikuje, są często przy robieniu takich rzeczy, jak sterowniki urządzeń i jądra systemu operacyjnego, które są zasadniczo bardzo niskie. Kiedy piszesz system wyższego poziomu, możesz również użyć funkcji wyższego poziomu C i uniknąć złożoności niskiego poziomu.

Jedną bardzo interesującą rzeczą, którą możesz przejrzeć, jest kod źródłowy C dla Ruby. W dokumentacji API Ruby (http://www.ruby-doc.org/core-1.9.3/) możesz kliknąć i zobaczyć kod źródłowy różnych metod. Interesujące jest to, że ten kod wygląda całkiem ładnie i elegancko - nie wygląda na tak skomplikowany, jak można sobie wyobrazić.

asc99c
źródło
... możesz także użyć funkcji wyższego poziomu C ... ”, ponieważ istnieją? ;-)
alk
Mam na myśli wyższy poziom niż manipulacja bitami i kreator wskaźnika do wskaźnika, który zwykle widzisz w kodzie typu sterownika urządzenia! A jeśli nie martwisz się narzutem kilku wywołań funkcji, możesz stworzyć kod C, który naprawdę wygląda na dość wysokim poziomie.
asc99c,
... możesz stworzyć kod C, który naprawdę wygląda na stosunkowo wysokim poziomie. ”, absolutnie, w pełni się z tym zgadzam. Ale chociaż „ ... funkcje wyższego poziomu ... ” nie należą do C, ale w twojej głowie, prawda?
alk
2

To, co zrobiłem, to oddzielenie kodu zależnego od urządzenia od kodu niezależnego od urządzenia, a następnie przetestowanie kodu niezależnego od urządzenia. Z dobrej modułowości i dyscypliny, można będzie skończyć z przeważnie dobrze przetestowane kodzie.

Paul Nathan
źródło
2

Nie ma powodu, dla którego nie możesz. Problem polega na tym, że mogą nie istnieć ładne „gotowe” ramy do testowania jednostek, takie jak w innych typach oprogramowania. W porządku. Oznacza to po prostu, że do testowania należy zastosować podejście typu „zrób to sam”.

Na przykład może być konieczne zaprogramowanie oprzyrządowania w celu uzyskania „fałszywych danych wejściowych” dla konwerterów A / D lub może być konieczne wygenerowanie strumienia „fałszywych danych” dla urządzenia osadzonego, na które będzie reagować.

Jeśli napotkasz opór przed użyciem słowa „TDD”, nazwij go „DVT” (test weryfikacji projektu), co sprawi, że EE będzie bardziej komfortowo z tym pomysłem.

Angelo
źródło
0

Czy możliwe jest nawet wykonywanie TDD na urządzeniach wbudowanych lub podczas opracowywania sterowników lub innych niestandardowych programów ładujących itp.?

Jakiś czas temu musiałem napisać program ładujący pierwszego poziomu dla procesora ARM. W rzeczywistości jest jeden z facetów, którzy sprzedają ten procesor. Użyliśmy schematu, w którym ich bootloader uruchamia nasz bootloader. Ale było to powolne, ponieważ musieliśmy flashować dwa pliki do NOR flash zamiast jednego, musieliśmy zbudować rozmiar naszego bootloadera w pierwszym bootloaderze i odbudować go za każdym razem, gdy zmieniliśmy nasz bootloader i tak dalej.

Postanowiłem więc zintegrować funkcje ich bootloadera z naszym. Ponieważ był to kod handlowy, musiałem się upewnić, że wszystko działa zgodnie z oczekiwaniami. Zmodyfikowałem więc QEMU, aby emulować bloki IP tego procesora (nie wszystkie, tylko te dotykające bootloadera) i dodałem kod do QEMU, aby „printf” wszystko czytać / zapisywać do rejestrów kontrolujących takie rzeczy jak PLL, UART, kontroler SRAM i wkrótce. Następnie zaktualizowałem nasz bootloader do obsługi tego procesora, a następnie porównałem dane wyjściowe, które dają naszemu bootloaderowi i ich emulatorowi, to pomaga mi złapać kilka błędów. Został napisany częściowo w asemblerze ARM, częściowo w C. Również po tym zmodyfikowane QEMU pomogło mi złapać jeden błąd, którego nie mogłem złapać przy użyciu JTAG i prawdziwego procesora ARM.

Więc nawet z C i asemblerem możesz używać testów.

Evgeniy
źródło
-2

Tak, możliwe jest wykonanie TDD na wbudowanym oprogramowaniu. Osoby twierdzące, że nie jest to możliwe, nie dotyczy lub nie dotyczy, są niepoprawne. Istnieje wiele korzyści z TDD wbudowanych jak w każdym oprogramowaniu.

Najlepszym sposobem na to jest jednak nie uruchamianie testów na celu, ale wyodrębnianie zależności sprzętowych oraz kompilowanie i uruchamianie na komputerze hosta.

Kiedy robisz TDD, będziesz tworzyć i przeprowadzać wiele testów. Potrzebujesz oprogramowania, które Ci w tym pomoże. Chcesz mieć szkielet testowy, dzięki któremu będzie to łatwe i szybkie dzięki automatycznemu wykrywaniu testów i generowaniu próbnego.

Najlepszą opcją dla C jest teraz Ceedling. Oto post, o którym pisałem na ten temat:

http://www.electronvector.com/blog/try-embedded-test-driven-development-right-now-with-ceedling

I jest wbudowany w Ruby! Nie musisz jednak znać żadnego Ruby, aby z niego korzystać.

Cherno
źródło
odpowiedzi powinny stać na własną rękę. Zmuszanie czytelników do dostępu do zasobów zewnętrznych w celu ustalenia, że ​​substancja jest marszczona na Stack Exchange („przeczytaj artykuł lub sprawdź Ceedling”). Zastanów się nad edycją, aby dopasować ją do norm jakości witryny
komnata
Czy Ceedling ma jakieś mechanizmy do obsługi zdarzeń asynchronicznych? Jednym z trudniejszych aspektów aplikacji wbudowanych w czasie rzeczywistym jest to, że zajmują się one odbieraniem danych wejściowych z bardzo złożonych systemów, które same są trudne do modelowania ...
Jay Elston
@Jay Nie ma nic specjalnie na to. Miałem jednak sukces w testowaniu tego rodzaju kpiny i konfigurowaniu architektury, która by to obsługiwała. Na przykład ostatnio pracowałem nad projektem, w którym zdarzenia sterowane przerwaniami zostały umieszczone w kolejce, a następnie wykorzystane w maszynie stanów „obsługi zdarzeń”. Zasadniczo była to funkcja wywoływana za każdym razem, gdy miało miejsce zdarzenie. Podczas testowania tej funkcji mogłem wyśmiewać wywołanie funkcji, które wyciągało zdarzenia z kolejki, a zatem mogłem symulować każde zdarzenie występujące w systemie. Pomaga tu również jazda próbna.
cherno