Zasoby na temat pisania wydajnego kodu C dla mikrokontrolerów? [Zamknięte]

15

Potrzebna tutaj poważna pomoc. Uwielbiam programować. Ostatnio czytałem wiele książek (takich jak K&R) i artykułów / forów internetowych dla języka C. Próbowałem nawet zaglądać do kodu Linuksa (chociaż zgubiłem się od czego zacząć, ale zaglądanie do małych bibliotek pomogło?).

Zaczynałem jako programista Java i w Javie jest dość wytrawny i suchy; Jeśli programy stają się zbyt duże, podziel je na klasy, a następnie na funkcje. Wytyczne takie jak: utrzymuj kod czytelny i dodawaj komentarze. Używaj ukrywania informacji i technik OOP. Niektóre z nich nadal dotyczą C.

Teraz koduję w C i do tej pory mam programy działające w ten czy inny sposób. Wiele osób mówi o wydajności / wydajności, algorytmie / projekcie, optymalizacji i łatwości konserwacji. Niektórzy ludzie podkreślają się bardziej niż inni, ale dla nieprofesjonalnych inżynierów oprogramowania często słyszysz coś takiego: np. Deweloper jądra Linuksa nie bierze tylko kodu.

Moje pytanie brzmi: planuję napisać kod dla 8-bitowego mikrokontrolera bez marnowania zasobów . Wiedz, że pochodzę z java, więc nie jest już tak samo ... zasoby / książki / linki / porady będą mile widziane. Teraz liczy się wydajność i rozmiar. Zasoby / porady dotyczące wydajnego (w ramach najlepszych praktyk) kodu C dla 8-bitowych mikrokontrolerów?

Ponadto, inline assemblyodgrywa istotną rolę jako dobrze trzymać blisko do mikro-kontrolerów standardowych. Ale czy istnieje ogólna ogólna zasada skuteczności, która dotyczy wszystkich?

Na przykład: register unsigned int variable_name;jest preferowane w chardowolnym momencie. Lub używanie, uint8_t jeśli nie potrzebujesz dużych liczb.

EDYCJA: Dziękuję bardzo za wszystkie odpowiedzi i sugestie. Doceniam wysiłek każdego, by dzielić się wiedzą.

AceofSpades
źródło
2
Nie dzieje się tak często w przypadku procesora x86, ale w przypadku mikrokontrolera, jeśli chcesz mieć pewność, że uzyskasz ostatni spadek wydajności, prawdopodobnie zamiast tego chcesz użyć asemblera.
Rei Miyasaka
3
@Rei: Ręcznie wykonany zespół rzadko, jeśli w ogóle, zużywa mniej pamięci i jest szybszy niż współczesne kompilatory C.
Strata
1
@mattnz Jak nowoczesny jesteś, o czym mówisz? Szczerze mówiąc, nie napisałem kodu dla mikrokontrolera od prawie dekady.
Rei Miyasaka
2
Jedna prosta wskazówka: w mikrokontrolerach nadal ogólnie jest prawdą, że „jeśli jest to prostsze, to jest szybsze”. W przypadku złożonych układów (ARM i wyższe) sprzęt wykonuje tak wiele optymalizacji, których nigdy nie znasz, dopóki nie przetestujesz.
Javier
1
@ReiMiyasaka przy ogromnym wzroście kosztów utrzymania. Dobry kompilator C może wygenerować prawie taki sam kod jak wysoce doświadczony programista.

Odpowiedzi:

33

Mam ponad 20 lat systemów wbudowanych, głównie 8 i 16 mikrosów. Krótka odpowiedź na twoje pytanie jest taka sama jak w przypadku innych programów - nie optymalizuj, dopóki nie będziesz wiedział, że musisz, a następnie nie optymalizuj, dopóki nie dowiesz się, co należy zoptymalizować. Napisz kod, aby był najpierw niezawodny, czytelny i łatwy w utrzymaniu. Przedwczesna optymalizacja jest równie dużym, jeśli nie większym, problemem w systemach wbudowanych

Kiedy programujesz „bez marnowania zasobów”, czy uważasz swój czas za zasób? Jeśli nie, kto płaci za twój czas, a jeśli nikt, nie masz z tym nic lepszego do roboty. Po dokonaniu wyboru, każdy projektant systemów wbudowanych musi dokonać wyboru kosztu sprzętu w stosunku do kosztu czasu inżynierskiego. Jeśli będziesz wysyłał 100 jednostek, użyj większej mikro, przy 100 000 jednostek, oszczędność 1,00 USD na jednostkę to tyle samo, co roczny rozwój oprogramowania (ignorowanie czasu wprowadzenia produktu na rynek, kosztów alternatywnych itp.), Przy 1 milionie jednostek zaczynasz uzyskiwanie zwrotu z inwestycji za obsesję na punkcie wykorzystania zasobów, ale bądź ostrożny, ponieważ wiele projektów osadzonych nigdy nie osiągnęło wartości 1 miliona, ponieważ zamierzały sprzedać 1 milion (wysoka początkowa inwestycja przy niskich kosztach produkcji) i zbankrutowały, zanim tam dotarły.

To powiedziawszy, rzeczy, które musisz wziąć pod uwagę (małe) systemy wbudowane, ponieważ przestaną one działać w nieoczekiwany sposób, a nie tylko spowolnią.

a) Stack - zazwyczaj masz mały rozmiar stosu i często ograniczony rozmiar ramki stosu. Musisz zawsze mieć świadomość tego, jakie jest twoje wykorzystanie stosu. Ostrzegamy, problemy ze stosami powodują jedne z najbardziej podstępnych wad.

b) Sterty - znowu, małe rozmiary sterty, więc uważaj na nieuzasadnioną alokację pamięci. Fragmentacja staje się problemem. Dzięki tym dwóm musisz wiedzieć, co robisz, gdy zabraknie - nie dzieje się tak w dużym systemie, ponieważ system stronicowania zapewnia system operacyjny. tzn. kiedy malloc zwraca NULL, czy sprawdzasz to i co robisz. Każdy ślaz potrzebuje kontroli i obsługi, wzdęcia kodu ?. Jako przewodnik - nie używaj go, jeśli istnieje alternatywa. Z tych powodów większość małych systemów nie korzysta z pamięci dynmaicznej.

c) Przerwy sprzętowe - Musisz wiedzieć, jak sobie z nimi radzić w bezpieczny i terminowy sposób. Musisz także wiedzieć, jak zrobić bezpieczny kod do ponownego wejścia. Na przykład standardowe biblioteki C na ogół nie są ponownie uruchamiane, więc nie należy ich używać w programach obsługi przerwań.

d) Montaż - prawie zawsze przedwczesna optymalizacja. Co najwyżej niewielka ilość (wstawiona) jest potrzebna do osiągnięcia czegoś, czego C po prostu nie może zrobić. W ramach ćwiczenia napisz małą metodę w ręcznie wykonanym zestawie (od zera). Zrób to samo w C. Zmierz wydajność. Założę się, że C będzie szybszy, wiem, że będzie bardziej czytelny, łatwy w utrzymaniu i rozszerzalny. Teraz część 2 ćwiczenia - napisz użyteczny program w asemblerze i C.
Jako kolejne ćwiczenie, zobacz, ile jądra Linuksa stanowi asembler, a następnie przeczytaj poniższy akapit o jądrze Linux-a.

Warto wiedzieć, jak to zrobić, może nawet warto biegle posługiwać się językami dla jednego lub dwóch popularnych mikrofonów.

e) „register unsigned int nazwa_zmiennej”, „register” jest i zawsze była wskazówką dla kompilatora, a nie instrukcją, na początku lat 70. (40 lat temu), miało to sens. W 2012 r. Marnuje się naciskanie klawiszy, ponieważ kompilatory są tak inteligentne, a zestawy instrukcji mikros tak złożone.

Wracając do twojego komentarza na temat Linuksa - problem, który tu masz, polega na tym, że nie mówimy tylko o 1 milionie jednostek, mówimy o setkach milionów, z czasem życia na zawsze. Warto poświęcić czas i koszty inżynierii, aby uzyskać optymalny poziom, jak to tylko możliwe. Chociaż jest to dobry przykład najlepszych praktyk inżynieryjnych, komercyjne samobójstwo dla większości twórców systemów wbudowanych byłoby tak pedantyczne, jak wymaga jądro Linuksa.

mattnz
źródło
4
mattnz: jest to jedna z najpiękniejszych odpowiedzi na stronach .stackexchange.
Ahmed Masud
1
Nie mogę poprawić tej odpowiedzi. Mogę tylko dodać, że wstawianie kodu asemblera rzadko ma sens ze względu na wydajność, ale może mieć sens w takich przypadkach, jak wtykanie układów I / O lub inne sztuczki sprzętowe, które mogą nie być łatwe do wykonania w C.
Mike Dunlavey
@mattnz Dzięki za dobrze postawioną odpowiedź. +1
AceofSpades
1
@MikeDunlavey asembler jest czasem potrzebny do dokładnego pomiaru czasu. Właśnie ukończyłem nakładkę wideo, która wykorzystuje bit banging do generowania wideo NTSC na pinie I / O, taktowanie jest w kategoriach - wysokiego napięcia dla 3 cykli zegara, a następnie niskiego dla 6, a następnie ...
Martin Beckett
@Martin: To ma sens. Dawno nie kodowałem na tym poziomie.
Mike Dunlavey
3

Twoje pytanie („bez marnowania zasobów”) jest zbyt ogólne, więc trudno jest udzielić wielu rad. Mówiąc dosłownie, jeśli nie chcesz marnować zasobów, być może powinieneś cofnąć się o krok i ocenić, czy w ogóle musisz coś zrobić, tj. Czy możesz rozwiązać problem na inne sposoby.

Przydatna rada zależy również od twoich ograniczeń - jaki system budujesz i jakiego procesora używasz? Czy to trudny system czasu rzeczywistego? Ile masz pamięci na kod i dane? Czy obsługuje natywnie wszystkie operacje C (w szczególności mnożenie i dzielenie) i dla jakich typów? Mówiąc bardziej ogólnie: przeczytaj cały arkusz danych i zrozum go.

Najważniejsza rada: zachowaj prostotę.

Np .: zapomnij o skomplikowanych strukturach danych (skróty, drzewa, ewentualnie nawet listy połączone) i użyj tablic o stałym rozmiarze. Korzystanie z bardziej skomplikowanych struktur danych jest uzasadnione dopiero po udowodnieniu przez pomiar, że tablice są zbyt wolne.

Nie przesadzaj z projektowaniem (coś, co deweloperzy Java / C # mają tendencję do robienia): pisz prosty kod proceduralny, bez zbytniego nakładania warstw. Abstrakcja ma swój koszt!

Zapoznaj się z pomysłem używania zmiennych globalnych i goto [bardzo przydatne do porządków w przypadku braku wyjątków];)

Jeśli masz do czynienia z przerwaniami, przeczytaj o ponownym przeniesieniu. Pisanie kodu ponownego wydania jest bardzo nietrywialne.

zvrba
źródło
3

Zgadzam się z odpowiedzią Mattnza - w przeważającej części. Zacząłem programować na 8085 ponad 30 lat temu, potem Z80, a następnie szybko migrowałem do 8031. Potem poszedłem do mikrokontrolerów z serii 68300, potem 80x86, XScale, MXL i (jak na koniec) 8-bitowych PICS, które ja zgadnij oznacza, że ​​zatoczyłem pełne koło. Dla przypomnienia, mogę stwierdzić, że FAE u kilku głównych producentów mikroprocesorów nadal używa asemblera, choć w sposób obiektowy w celu celowego ponownego wykorzystania kodu.

W zatwierdzonej odpowiedzi nie widzę dyskusji na temat typu procesora docelowego i / lub proponowanej architektury. Czy to gorzki 0,50 $ 8 z ograniczoną pamięcią? Czy jest to rdzeń ARM9 z potokowaniem i 8 megapikselami flash? Koprocesor zarządzania pamięcią? Czy będzie miał system operacyjny? Pętla while (1)? Urządzenie konsumenckie z początkowym przebiegiem produkcyjnym 100 000 sztuk? Firma rozpoczynająca działalność z dużymi pomysłami i marzeniami?

Chociaż zgadzam się, że nowoczesne kompilatory wykonują świetną robotę optymalizacyjną, nigdy nie pracowałem nad projektem od 30 lat, w którym nie zatrzymałem debugera i nie zobaczyłem wygenerowanego kodu asemblera, aby zobaczyć, co się dzieje pod maską ( co prawda koszmar, gdy w grę wchodzą rurociągi i optymalizacja), więc znajomość montażu jest ważna.

I nigdy nie miałem CEO, wiceprezesa ds. Inżynierii, klienta, który nie zmusiłby mnie do wypchnięcia galona do pojemnika na kwarty lub zaoszczędzenia 0,05 $ za pomocą rozwiązania programowego w celu rozwiązania problemu sprzętowego (hej, to tylko oprogramowanie prawda? Co jest takie trudne?). Optymalizacja pamięci (kodu lub danych) zawsze będzie się liczyć.

Chodzi mi o to, że jeśli spojrzysz na projekt z czysto programowego punktu widzenia, uzyskasz mądrzejsze rozwiązanie. Mattnz ma rację - spraw, by działał, a następnie spraw, aby działał szybciej, mniej, lepiej, ale wciąż musisz poświęcić DUŻO czasu na wymagania i rezultaty, zanim nawet pomyślisz o kodowaniu.

Gio
źródło
Cześć Gio, unikaj niepotrzebnego HTMLa w swoich postach i zamiast tego użyj składni Markdown . Dla <br />może po prostu naciśnij enter, a do ust po prostu zostawić pustą linię między nimi. Również, gdy odwołujesz się do innej odpowiedzi, dodaj link do niej. W tym momencie może być tylko kilka odpowiedzi, ale może być ich więcej, rozmieszczonych na wielu stronach i nie będzie jasne, która odpowiedź masz na myśli. Sprawdź historię zmian, aby zobaczyć moje zmiany.
yannis
@Gio Dziękujemy za wymienienie innych ważnych czynników. +1 :)
AceofSpades
+1 - Ładne rozwinięcie mojej odpowiedzi.
mattnz
1

Odpowiedź Manttza bardzo dobrze określa najważniejsze kwestie związane z programowaniem „blisko sprzętu”. Po to właśnie jest przeznaczone C.

Chciałbym jednak dodać, że chociaż ścisłe słowo kluczowe „Class” nie istnieje w C - myślenie w kategoriach programowania obiektowego w C jest całkiem proste, nawet jeśli jesteś blisko sprzętu.

Możesz rozważyć tę odpowiedź: OO najlepszych praktyk dla programów C, która wyjaśnia ten punkt.

Oto niektóre zasoby, które pomogą Ci napisać dobry kod obiektowy w C.

za. Programowanie obiektowe w C
b. jest to dobre miejsce, w którym ludzie wymieniają się pomysłami
c. a oto pełna książka

Innym dobrym zasobem, który chciałbym zasugerować, jest:

Napisz wielką serię kodów . To jest książka o dwóch tomach. Pierwsza książka dotyczy bardzo istotnych aspektów pracy maszyn na niższych poziomach. Druga książka dotyczy „Myślenia na niskim poziomie - pisania na wysokim poziomie”

Dipan Mehta
źródło
1

Masz kilka problemów. najpierw chcesz, aby ten projekt / kod był przenośny? Przenośność kosztuje Twoją wydajność i rozmiar. Czy twoja platforma wyboru i realizowane zadanie mogą tolerować nadmiar wielkości i niższą wydajność?

Tak, absolutnie na 8-bitowej maszynie zwracanie znaków bez znaku zamiast znaków lub skrótów bez znaku, jest jednym ze sposobów na poprawę wydajności i rozmiaru. Podobnie na maszynie 16-bitowej używaj skrótów bez znaku i 32-bitowej maszyny bez znaku int. Można jednak dość łatwo zauważyć, że jeśli na przykład po prostu wszędzie używałeś niepodpisanych int dla przenośności w całym przejmującym systemie (ARM na przykład spychając w dół do najniższej mocy, najmniejszych rynków urządzeń), kod ten jest ogromną świnią 8-bitowy mikro. Oczywiście możesz po prostu użyć niepodpisanego bez int, short lub char i pozwolić kompilatorowi wybrać optymalny rozmiar.

Nie tylko wbudowany zestaw, ale ogólnie język asemblera. Zespół wbudowany jest bardzo nieprzenośny i trudniej go pisać, niż tylko wywoływanie funkcji asm. tak, spalasz konfigurację połączenia i powrót, ale kosztem łatwiejszego rozwoju, lepszej konserwacji i kontroli. Reguła nadal obowiązuje, zapisz ją tylko w asm, jeśli naprawdę tego potrzebujesz, abyś doszedł do wniosku, że wyjście kompilatora jest twoim problemem w tym obszarze i ile wzrostu wydajności można uzyskać, wykonując to ręcznie. Następnie wróć do przenośności i konserwacji, jak tylko zaczniesz mieszać C i asm, i za każdym razem, gdy mieszasz C i asm, możesz zaszkodzić swojej przenośności i może sprawić, że projekt będzie trudniejszy do utrzymania w zależności od tego, kto nad nim pracuje lub jeśli to to produkt, który teraz opracowujesz, a ktoś inny musi utrzymać go na drodze. Po wykonaniu tej analizy automatycznie wiesz, czy musisz iść w linii, czy iść z prostym montażem. Mam ponad 25 lat w terenie, codziennie piszę mieszanki C i asm, żyję na / w warstwie sprzętowej / programowej i, cóż, nigdy nie używam wbudowanego asm. Rzadko jest to warte wysiłku, zbyt specyficzny dla kompilatora, piszę kod niespecyficzny dla kompilatora, gdziekolwiek jest to możliwe (prawie wszędzie).

Kluczem do całego pytania jest rozmontowanie kodu C. Dowiedz się, co kompilator robi z twoim kodem, az czasem, jeśli chcesz, możesz nauczyć się manipulować kompilatorem w celu generowania kodu, który chcesz, bez konieczności uciekania się do asm. Więcej czasu możesz nauczyć się manipulować kompilatorem w celu tworzenia wydajnego kodu w wielu obiektach docelowych, dzięki czemu kod jest bardziej przenośny bez konieczności uciekania się do asm. Nie powinieneś mieć problemu ze zrozumieniem, dlaczego znak bez znaku działa lepiej jako zwrot statusu z funkcji niż znak bez znaku na 8-bitowej mikro, podobnie znak bez znaku staje się droższy w systemach 16 i 32-bitowych (niektóre architektury pomagają ci out, niektóre nie).

Niektóre 8-bitowe mikrokontrolery, wszystkie ?, są bardzo nieprzyjazne dla kompilatora i żaden kompilator nie wytwarza dobrego kodu. Nie ma wystarczającego zapotrzebowania na stworzenie rynku kompilatorów dla tych urządzeń, aby stworzyć świetny kompilator dla tych celów, więc kompilatory, które tam są, przyciągają więcej biznesowych, nie-asmowych programistów, i nie dlatego, że kompilator jest lepszy niż ręcznie napisany asm . arm i mips wchodzące na ten świat zmieniają ten model, ponieważ masz cele, które mają kompilatory, które wykonały wiele pracy, kompilatory, które produkują całkiem niezły kod itp. W przypadku mikroprocesorów z takimi procesorami oczywiście nadal masz w przypadku, gdy musisz zejść do asm, ale nie jest to tak często, o wiele łatwiej jest po prostu powiedzieć kompilatorowi, co chcesz zrobić, niż go nie używać. Pamiętaj, że manipulowanie kompilatorem nie jest brzydkim, nieczytelnym kodem, w rzeczywistości jest to odwrotny, ładny czysty, prosty kod, być może zmieniający kilka elementów. Kontrolowanie wielkości twoich funkcji i liczby parametrów ma ogromny wpływ na wydajność kompilatora. Unikaj funkcji ghee-whiz kompilatora lub języka, KISS, bądź głupi, często produkuje znacznie lepszy i szybszy kod.

old_timer
źródło
Nie podajesz rodzaju wytwarzanych produktów. Zakładam, że jest to albo bardzo duży wolumen, niska marża, albo specyficzny rynek niszowy z szalonymi marżami. Ma to kluczowe znaczenie przy podejmowaniu decyzji na poziomie biznesowym, czy powinieneś użyć małego 8-bitowego mikro asemblera ręcznie, lub większego mikro i C. W odpowiedzi na Twój (usunięty?) Komentarz - pracuję z 8-bitowymi mikrami, jednak my zacznij od większej niż potrzebna mikro i skoryguj w dół tylko wtedy, gdy koszt BOM stanie się problemem) Czas na rynek, koszt alternatywny i zamortyzowany koszt opracowania pozwalają nam na luksus dodania 10 lub 20 centów do BOM.
mattnz