Pokażę tutaj jak przyśpieszyłem programową komunikację SPI służącą do obsługi pamięci Flash. Sprawdzę jak dużo kodu może kryć się za warstwą abstrakcji GPIO BK7231 a potem spróbuję operacje na pinach zredukować tak, by działały bezpośrednio na rejestrach.
Ile cykli procesora tracone jest na zbędne operacje? Ilukrotnie szybsza będzie nowa wersja komunikacji? Przekonajmy się!
Ten temat jest kontynuacją mojej prezentacji obsługi pamięci Flash programowo:
Dodatkowa pamięć Flash na pomiary za darmo? Protokół komunikacji, zapis, odczyt, kasowanie
Punktem wyjściowym jest to, co tam zaprezentowałem.
Przypomnijmy zatem - to są moje funkcje SPI, które chcę zoptymalizować:
Kod: C / C++
Skupmy się na wysyłaniu. Pierwszym krokiem jest sprawdzenie co robi HAL_PIN_SetOutputValue. Za tę funkcję jestem odpowiedzialny ja sam, to fragment mojego HAL, czyli wartwy abstrakcji dla różnych platform.
Kod: C / C++
Reszta jest w rękach Chińczyków. Sprawdźmy co robi bk_gpio_output i jak dużo kodu jest pod maską.
Kod: C / C++
Dość dużo się tutaj dzieje. Sterownik GPIO jest schowany za funkcją sddev_control, a argument (pin i wartość) jest kodowany w 32-bitowym integerze. Pierwszy bajt to numer pinu, drugi to wartość.
Funkcja niby jest oznaczona jako inline, czyli kompilator może ją zamienić na kod umieszczony tam gdzie ją wywołujemy, ale to dużo nie zmienia.
Co zatem robi sddev_control?
Kod: C / C++
To wygląda gorzej niż myślałem. Tu jest cała tablica sterowników a każdy z nich jest określany nazwą tekstową - łańcuchem znaków. Odpowiedni sterownik jest wyszukiwany a potem dopiero wywoływana jest obsługa danego polecenia przez wskaźnik na funkcję.
Mamy tu wręcz zagnieżdżone pętle - porównywanie znaków dla każdego z wpisów...
Ciekawe, jaka jest wartość DD_MAX_SDEV? Z tego co widzę, to 16. Nie będzie więcej tych sterowników niż 16.
Dalej musiałem wyszukać odpowiedni sterownik i jego funkcję obsługującą poszukiwaną komendę:
Kolejny switch... co prawda to wartości liczbowe, ale i tak mamy tu kolejne rozgałęzienia.
Już domyślam się, co robią funkcje DEMUX. Tak jak wcześniej kodowano pin i wartość do jednego integera, to tutaj są one dekodowane.
Kod: C / C++
Zasadniczo wracamy do punktu wyjścia - nie wiem po co ten cały system sterowników był. Kolejna funkcja do sprawdzenia to gpio_output.
Kod: C / C++
Tu już coś się dzieje... ale jest poprzedzone kolejnym, potencjalnie zbędnym wywołaniem. Filtrowanie GPIO - blokada JTAG?
Kod: C / C++
W sumie ma to sens. To jest blokada przed rekonfiguracją pinów JTAG gdy się z nich korzysta.
Zostaje nam:
Kod: C / C++
Kod: C / C++
Wyjaśnijmy najpierw co jest czym:
- REG_WRITE to makro które po prostu przypisuje wartość do komórki pod danym adresem
- REG_READ - analogicznie, odczyt
- GCFG_OUTPUT_POS to pozycja bitu określającego wyjście na danym pinie - tutaj 1
- GCFG_OUTPUT_BIT to ten wspomniany bit wyjścia
A więc ten cały fragment można uprościć. Nie jest potrzebne badanie wartości val, nie jest potrzebne przesuwanie, nie jest potrzebny odczyt wartości pinu.
Zostaje pytanie, czy pozostałe wartości rejestru coś dla nas znaczą. Sprawdziłem to eksperymentalnie. Ostatecznie swoją szybką operację na GPIO rozdzieliłem na dwa etapy.
1. Zamiana indeksu pinu na wskaźnik na rejestr:
Kod: C / C++
2. Ustawienie pinu:
Kod: C / C++
To teraz starcza, by ustawiać wysoki lub niski stan na danym pinie. Przykładowo, dla SPI, zostaje tylko wolna inicjalizacja:
Kod: C / C++
Reszta jest znacznie przyśpieszona - mało znaczące, ale jednak obecne przestawienie pinu CS/SS (Chip Select/Slave Select):
Kod: C / C++
Wysyłanie bitu przez SPI też jest znacznie szybsze, jedynie muszę sprawdzać czy dany bit jest zapalony:
Kod: C / C++
Wysyłanie bajtu przy okazji wyciągnąłem z pętli i rozpisałem ręcznie, aby unikać operacji na zmiennej od iteracji:
Kod: C / C++
Analogicznie uprościłem odczyt.
Kod: C / C++
Kod: C / C++
Tyle optymalizacji starczy - pora na testy. Co prawda mój pomiar czasu na BK7231 nie jest zbyt dokładny, ale myślę, że bez wątpienia uzyskam dobre rezultaty.
Czas mierzyłem dla omówionej w poprzedniej części procedury testowej erase/read/write/read, która czyści pamięć, weryfikuje to, zapisuje do niej wzór, a potem go odczytuje i porównuje.
Wersja zwykła - 8kB - 11.5 sekund:
Wersja przyśpieszona - 8kB - około 0.4 sekundy.
Niemalże 30 razy szybciej!
Sprawdziłem jeszcze z większą ilością danych.
Wersja zwykła - 64kB - nie doczekałem się rezultatu.
Wersja przyśpieszona - 64kB - około 3.5 sekund.
Podsumowując, zdecydowanie warto było zdjąć warstwę HAL z operacji na GPIO i dokonywać ich bezpośrednio poprzez dostęp do rejestrów. Przyśpieszenie jest wyraźnie odczuwalne i replikowalne, nowa wersja wykonuje operację około 28 razy szybciej. Możliwe, że dałoby się jeszcze bardziej przyspieszyć, ale do moich zastosowań to jak najbardziej starcza już w bieżącej formie.
Oczywiście, to samo podejście można zastosować do wielu innych platform - chociażby samo digitalWrite na Arduino cierpi na podobną przypadłość.
Czy zdarzało Wam się optymalizować w ten sposób komunikację?
[i]PS: Przepraszam za ten kod w formie obrazka w jednej sekcji tematu, ale inaczej się nie dało - włączał on ochronę antyspamową forum...
Fajne? Ranking DIY Pomogłem? Kup mi kawę.