logo elektroda
logo elektroda
X
logo elektroda
REKLAMA
REKLAMA
Adblock/uBlockOrigin/AdGuard mogą powodować znikanie niektórych postów z powodu nowej reguły.

Jak przyspieszyć programowe SPI do obsługi pamięci Flash? Bezpośrednie operacje na rejestrach

p.kaczmarek2 09 Sie 2025 12:47 582 4

TL;DR

  • Optymalizacja programowego SPI na BK7231 do obsługi pamięci Flash, z naciskiem na przyspieszenie operacji GPIO.
  • Zamiast HAL_PIN_SetOutputValue użyto bezpośredniego zapisu do rejestrów GPIO, po rozdzieleniu indeksu pinu od wskaźnika rejestru.
  • Test erase/read/write/read dla 8 kB skrócił się z 11.5 sekund do około 0.4 sekundy.
  • Wersja przyspieszona dla 64 kB zakończyła się po około 3.5 sekundach, a zwykła nie doczekała rezultatu.
  • Pomiary na BK7231 były niedokładne, ale przyspieszenie oceniono jako około 28–30 razy.
Podsumowanie AI na podstawie dyskusji. Może zawierać błędy.
REKLAMA
📢 Słuchaj (Głos AI):
  • Zrzut ekranu przedstawiający kod funkcji gpio_ctrl z instrukcją switch dla CMD_GPIO_OUTPUT
    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++
    Zaloguj się, aby zobaczyć kod

    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++
    Zaloguj się, aby zobaczyć kod

    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++
    Zaloguj się, aby zobaczyć kod

    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++
    Zaloguj się, aby zobaczyć kod

    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ę:
    Zrzut ekranu przedstawiający kod funkcji gpio_ctrl z instrukcją switch dla CMD_GPIO_OUTPUT
    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++
    Zaloguj się, aby zobaczyć kod

    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++
    Zaloguj się, aby zobaczyć kod

    Tu już coś się dzieje... ale jest poprzedzone kolejnym, potencjalnie zbędnym wywołaniem. Filtrowanie GPIO - blokada JTAG?
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    W sumie ma to sens. To jest blokada przed rekonfiguracją pinów JTAG gdy się z nich korzysta.
    Zostaje nam:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    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++
    Zaloguj się, aby zobaczyć kod

    2. Ustawienie pinu:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    To teraz starcza, by ustawiać wysoki lub niski stan na danym pinie. Przykładowo, dla SPI, zostaje tylko wolna inicjalizacja:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    Reszta jest znacznie przyśpieszona - mało znaczące, ale jednak obecne przestawienie pinu CS/SS (Chip Select/Slave Select):
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    Wysyłanie bitu przez SPI też jest znacznie szybsze, jedynie muszę sprawdzać czy dany bit jest zapalony:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    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++
    Zaloguj się, aby zobaczyć kod

    Analogicznie uprościłem odczyt.
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    Kod: C / C++
    Zaloguj się, aby zobaczyć kod


    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:
    Konsola testowa SPI pokazująca wynik operacji Flash i czas 11544 ms
    Wersja przyśpieszona - 8kB - około 0.4 sekundy.
    Wynik testu SPI Flash pokazujący czas 432 ms dla operacji na 8192 bajtach
    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.
    Zrzut ekranu z interfejsu BK7231T_FourRelays pokazujący wyniki testu SPIFlash
    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ę.
    O autorze
    p.kaczmarek2
    Moderator Smart Home
    Offline 
  • REKLAMA
  • #2 21630136
    androot
    VIP Zasłużony dla elektroda
    Posty: 1801
    Pomógł: 54
    Ocena: 635
    Ciekawy artykuł. BK7231 nie ma sprzętowych SPI? Rozumiem że to na wypadek gdyby były zajęte? Normalnie raczej nie macha się szybko pinami GPIO. Myślę że warto sprawdzić te funkcje które są krytyczne jeśli chodzi o szybkość wykonywania się, ale w moim przypadku na platformie ESP32 S3.
  • REKLAMA
  • #3 21630152
    p.kaczmarek2
    Moderator Smart Home
    Posty: 14706
    Pomógł: 658
    Ocena: 12766
    BK7231 ma jedno sprzętowe SPI, ale znajduje się ono na pinach P14/P16/P17 i większość popularnych modułów (CB2S/CB3S/WB2S/WB3S) nie ma ich wyprowadzonych. Swego czasu pokazywałem lutowanie do padu QFN by jeden z tych pinów - P16 - wyprowadzić:
    Jak uzyskać dostęp do sprzętowego portu SPI w CB2S? P16 (MOSI) lutujemy do QFN
    Z tego P16 korzystamy najczęściej do obsługi indywidualnie adresowalnych LEDów, mamy do tego sterownik oparty o DMA:
    Sterowanie diodami WS2812 przez SPI z DMA - użycie MOSI do generowania timingów

    Chyba jedyny moduł z wyprowadzonymi pinami od SPI jaki kojarzę to CBU, ale spotykam go znacznie rzadziej w urządzeniach smart, więc jest o co powalczyć z metodą programową.

    Oczywiście, użyteczność to jedna sprawa, a druga sprawa to też wartość edukacyjna i sama możliwość przekonania się za jak dużym (i często zbędnym?) HAL (warstwą abstrakcji) znajdują się różne funkcje. Te wyszukiwanie po nazwie tekstowej w sddev_control to nawet i mnie zaskoczyło.
    Pomogłem? Kup mi kawę.
  • REKLAMA
  • #4 21631744
    daniels7
    Poziom 13  
    Posty: 25
    Pomógł: 2
    Ocena: 1
    >>21630060 z tym Arduino to prawda... Zamiana w sketchu digitalWrite na operację atomową plus kilka innych prostych rzeczy zmienia tyle, że coś co nie miesciło się w Atmega328 zajmuje połowę pamięci Atmega168. No i przyspieszenie jest znaczące.
  • #5 21631789
    p.kaczmarek2
    Moderator Smart Home
    Posty: 14706
    Pomógł: 658
    Ocena: 12766
    A teraz pokrewny przykład - wysyłanie sygnałów IR. Tam jest analogiczna sytuacja na Beken, PWM też jest za dużym HAL, więc w swoim sterowniku do wysyłania danych zebranych Flipperem PRO: Budowa sygnału IR, przechwytywanie w Flipper Zero, zapis w formacie raw
    też "wyciągnąłem" operacje PWM na wierzch, było:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    jest:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    reg_duty już wcześniej jednokrotnie obliczam na bazie indeksu PWM.
    Całość wywołuję w ISR w timerze o domyślnym okresie 50us.
    Pomogłem? Kup mi kawę.
📢 Słuchaj (Głos AI):
REKLAMA