Ekspander GPIO w wymagającym środowisku - SX1509
November 17, 2023

W embedded software, zapalanie LEDów to proste zadanie, które może być wyzwaniem w projektach z ograniczoną przestrzenią na płytce drukowanej. Problemem jest brak pinów mikrokontrolera. Rozwiązaniem są ekspandery GPIO, pozwalające na obsługę wielu wejść/wyjść przez interfejsy szeregowe jak np. I2C.

Artykuł omawia wyzwania z opóźnieniami w sterowaniu LEDami przez ekspandery i alternatywy, np. dedykowane sterowniki LED. Przykładem jest układ SX1509, łączący sterowanie LEDami i obsługę przycisków.

WSTĘP
Zapalanie diod LED w projektach embedded jest często pierwszym zadaniem dla programistów, które, choć pozornie proste, może przynieść wiele wyzwań. W projektach dąży się do maksymalizacji liczby peryferiów podłączonych do jednego mikrokontrolera, minimalizując potrzebę dzielenia kodu i optymalizując przestrzeń na płytce drukowanej (PCB). Jednak taka konfiguracja może prowadzić do braku wystarczającej liczby pinów dla wszystkich funkcji urządzenia.

Rozwiązaniem są układy ekspanderów GPIO, które umożliwiają obsługę licznych wejść/wyjść przez interfejsy szeregowe jak np. I2C, potrzebując tylko dwóch linii komunikacyjnych. Ta metoda ma jednak swoje wyzwania, w tym opóźnienia. Bezpośrednie sterowanie pinami GPIO mikrokontrolera jest szybkie, co jest widoczne podczas użycia analizatorów stanów logicznych do profilowania kodu. W przypadku ekspanderów portów, czas potrzebny na sterowanie jest dłuższy z powodu konieczności szeregowego przesyłania komend i przetwarzania komunikacji w pamięci, co w niektórych sytuacjach może stanowić problem.

DLACZEGO?

W naszym projekcie stanęliśmy przed wyzwaniem obsługi wielu diod LED oraz kilku przycisków, korzystając z ekspandera portów. Zmiana mikrokontrolera nie była opcją - używaliśmy tego samego modelu MCU w różnych urządzeniach, co ułatwiało implementację. Podstawowa obsługa LEDów, taka jak włączanie i wyłączanie, jest stosunkowo prosta, ale zadanie komplikuje się, gdy wymagane jest sterowanie intensywnością światła i realizacja efektów płynnego włączania oraz wyłączania.

Do regulacji jasności diod stosuje się modulację szerokości impulsu (PWM), co wymaga szybkiego przełączania stanu pinów w precyzyjnych odstępach czasowych. Techniki takie jak bit-banging, gdzie procesor za każdym razem kontroluje stan pinu, są nieefektywne. Zazwyczaj wykorzystuje się układy liczników z trybem PWM. Jednak przy użyciu ekspandera portów, bit-banging staje się niemożliwy ze względu na czasochłonność zmian stanu pinu, a wykorzystanie timerów procesora nie jest możliwe.

Alternatywą mogą być dedykowane sterowniki LED, ale w naszym przypadku wymagałoby to dodatkowego układu do obsługi przycisków. Na szczęście istnieją układy łączące te funkcje. Przykładem jest SX1509 firmy Semtech.

SX1509

Układ SX1509 posiada wiele cech które były przydatne w naszym przypadku:

  • Niskie napięcie pracy oraz niski pobór prądu - nasze urządzenie pracuje na baterii więc cecha ta pozwala na wydłużenie pracy urządzenia;
  • Wbudowany sterownik LEDów z 256-stopniwym PWMem oraz programowalnym miganiem i "oddychaniem" (efekt fade-in/out);
  • Sprzętowy debouncing pinów wejściowych - pozwala uniknąć dodatkowych elementów filtrujących na PCB;
  • Wbudowany oscylator, dzięki czemu nie potrzeba generować osobnego sygnału zegara dla układu;
  • Dedykowana linia przerwań - pozwala na asynchroniczne odczytywanie zmian przycisków zamiast ciągłego odpytywania (polling);

Inne ciekawe funkcje które nie były w tym przypadku kluczowe:

  • Piny tolerujące 5V - pozwala to uprościć elektronikę gdy mamy do czynienia z logiką na wyższym napięciu niż reszta układu;
  • Niezależne napięcia na bankach pinów - dzięki temu układ można wykorzystać jako translator poziomów napięć;
  • Wbudowany układ skanowania macierzy przycisków - w sytuacjach kiedy ilość przycisków jest większa niż ilość pinów ekspandera;

IMPLEMENTACJA

SX1509 komunikuje się z procesorem za pośrednictwem magistrali I2C, wykorzystując standardowy protokół. Proces polega na wysłaniu adresu urządzenia (SX1509) i adresu rejestru, do którego chcemy zapisać lub z którego chcemy odczytać dane, a następnie wysłaniu lub odbiorze danych.

Podczas tworzenia uniwersalnego sterownika do ekspandera portów, kluczowe jest opracowanie efektywnego sposobu zapisu i odczytu stanów pinów GPIO. Najbardziej podstawowa metoda obejmuje każdorazowe wysyłanie komend zapisu lub odczytu do ekspandera, co ilustruje poniższy pseudokod. Pseudokod ten jest uproszczony i pomija szereg szczegółów, takich jak obsługa błędów, unikanie konfiguracji wejść w innym trybie czy wykorzystanie asynchronicznego API.


typedef uint16_t sx1509_channels_t; void sx1509_write(sx1509_channels_t state) { uint8_t buf[3] = {SX1509_REG_DATA_B, BYTE(1, state), BYTE(0, state)}; sx1509_i2c_write_start(buf, sizeof(buf)); sx1509_i2c_wait(); } sx1509_channels_t sx1509_read(void) { uint8_t tx[1] = {SX1509_REG_DATA_B}; uint8_t rx[2] = {0}; sx1509_i2c_read_start(tx, sizeof(tx), rx, sizeof(rx)); sx1509_i2c_wait(); return (rx[0] << 8) | rx[1]; }

Bardzo łatwo można jednak zoptymalizować nasz sterownik, przechowując kopię stanu wyjść ekspandera w pamięci naszego procesora, np.

static struct { sx1509_channels_t outputs; sx1509_channels_t inputs; } sx1509_state; void sx1509_set_output(sx1509_channels_t state, sx1509_channels_t mask) { state = (sx1509_state.outputs & ~mask) | (state & mask); uint8_t buf[3] = {SX1509_REG_DATA_B, BYTE(1, state), BYTE(0, state)}; sx1509_i2c_write_start(buf, sizeof(buf)); sx1509_i2c_wait(); state.outputs = state; } sx1509_channels_t sx1509_read(void) { return sx1509_state.inputs; } void sx1509_on_interrupt_pin(void) { uint8_t tx[1] = {SX1509_REG_DATA_B}; uint8_t rx[2] = {0}; sx1509_i2c_read_start(tx, sizeof(tx), rx, sizeof(rx)); sx1509_i2c_wait(); sx1509_state.inputs = (rx[0] << 8) | rx[1]; }

W naszym przypadku wykorzystujemy możliwość konfiguracji układu SX1509 do ustawiania stanu pinu przerwania przy zmianie stanu wejść. W procesie zapisu aktualizujemy jedynie naszą lokalną kopię stanu pinów, a przy odczycie odwołujemy się do tego zapisanego stanu. Aktualizacja stanu wejść odbywa się asynchronicznie, reagując na zmiany w pinie przerwania SX1509.

Dzięki tej prostej modyfikacji osiągamy kilka korzyści:

  • Natychmiastowy odczyt stanu pinów, ponieważ ich aktualny stan jest zawsze przechowywany w pamięci.
  • W naszym API zapisu istnieje możliwość maskowania, co pozwala wybrać, które wyjścia chcemy ustawić.
  • Dodajemy funkcję "toggle", która umożliwia ustawienie przeciwnych wartości na wyjściach bez konieczności dodatkowego odczytu stanu pinów przez I2C.

Chociaż to podejście może wydawać się oczywiste dla doświadczonych programistów, zawsze warto zwracać uwagę na takie detale, które mogą znacząco ułatwić implementację naszych sterowników.

PODSUMOWANIE

W świecie embedded, nawet tak proste zadania jak kontrolowanie LEDów mogą prowadzić do licznych wyzwań i decyzji. W naszym przypadku, konieczność użycia ekspandera portów GPIO zderzyła się z potrzebą zaawansowanej obsługi LEDów i wejść. Dzięki układowi SX1509, udało się uniknąć konieczności wykorzystania kilku odrębnych urządzeń, jednocześnie odciążając główny procesor.

Jednakże, każdy przypadek wymaga indywidualnej oceny. Jeśli priorytetem jest szybka obsługa stanu pinów, warto rozważyć użycie układów sterowanych przez SPI z wysoką częstotliwością lub dopasować projekt tak, aby można było bezpośrednio wykorzystać piny mikrokontrolera. Istotnym czynnikiem jest również nakład czasu potrzebny na implementację sterownika dla danego układu. Opracowanie uniwersalnego sterownika SX1509 z asynchronicznym API, z uwzględnieniem bezpieczeństwa wątków, może wymagać znacznego wysiłku.

RREFERNECJE