wtorek, 19 marca 2024

STM32H7 - Interface GPIO

W tym poście chciałbym opisać sposób wykonania sterownika pinów na przykładzie STM32H7. 


Na samym początku tworzymy strukturę, która będzie przechowywała parametry przekaźnika jakie będą mi potrzebne.

  1. typedef struct {  
  2.    _Bool Polar;  
  3.    _Bool Otwarcie;  
  4.    _Bool Zamkniecie;  
  5.    _Bool Kontrola;  
  6.    uint32_t CzasWejscia;  
  7.    uint32_t CzasOtwarcia;  
  8.    volatile uint32_t Licznik;
  9.    volatile uint32_t LicznikCzasOtwarcia;
  10.    uint8_t OpenAllTheTime;
  11. } KonfigKanalPrzekaznik_t;
  12.  
  13. typedef struct {           
  14.    GPIO_TypeDef *Port;
  15.    uint32_t Pin;
  16.    uint32_t ClockMsk;
  17.    uint8_t Stan;
  18.    KonfigKanalWyjscieFunkcja_t Funkcja;
  19.    KonfigKanalPrzekaznik_t  KonfigPrzekaznik;
  20. } KonfigKanalWyjscie_t;
  21.  
  22. extern KonfigKanalWyjscie_t KonfigKanalWyjscie[6];

Do interfejsu przypisałem funkcje, które bezpośrednio działają sprzęcie: 

  1. typedef struct {
  2.     void (*init)  (KonfigKanalWyjscie_t *KonfigKanalWyjsciePtr);
  3.     uint8_t (*read)  (GPIO_TypeDef *portPtr, const uint32_t pinMask);
  4.     uint8_t (*write) (GPIO_TypeDef *portPtr, const uint32_t pinMask, const uint8_t state, const _Bool polar);
  5.     uint8_t (*toggle)(GPIO_TypeDef *portPtr, const uint32_t pinMask);
  6. } Relay_ControlPin_i;
  7.  
  8. Relay_ControlPin_i RelayControl_i = {
  9.         .init = InitRelayPins,
  10.         .read = ReadRelayPins,
  11.         .write = WriteRelayPins,
  12.         .toggle = ToggleRelayPins,
  13. };

Interface pobieramy korzystając z funkcji zwracającej wskaźnik do niej:

  1. const Relay_ControlPin_i *relay_interface_get(void)
  2. {
  3.     return &RelayControl_i;
  4. }

Funkcja konfigurująca piny jako wyjścia:

  1. void InitRelayPins(KonfigKanalWyjscie_t *KonfigKanalWyjsciePtr) {
  2.     LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
  3.  
  4.     LL_AHB4_GRP1_EnableClock(KonfigKanalWyjsciePtr[0].ClockMsk);
  5.     LL_AHB4_GRP1_EnableClock(KonfigKanalWyjsciePtr[1].ClockMsk);
  6.     LL_AHB4_GRP1_EnableClock(KonfigKanalWyjsciePtr[2].ClockMsk);
  7.     LL_AHB4_GRP1_EnableClock(KonfigKanalWyjsciePtr[3].ClockMsk);
  8.     LL_AHB4_GRP1_EnableClock(KonfigKanalWyjsciePtr[4].ClockMsk);
  9.     LL_AHB4_GRP1_EnableClock(KonfigKanalWyjsciePtr[5].ClockMsk);
  10.  
  11.     for(uint8_t i=0; i<6; i++)
  12.     {
  13.         GPIO_InitStruct.Pin = KonfigKanalWyjsciePtr[i].Pin;
  14.         GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
  15.         GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_HIGH;
  16.         GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
  17.         GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
  18.         LL_GPIO_Init(KonfigKanalWyjsciePtr[i].Port, &GPIO_InitStruct);
  19.     }
  20. }

W tym przypadku piny są ustawiane w identyczny sposób. Jeśli są jakieś różnice w konfiguracji, to można je przerzucić do struktury. Przez co pozostałe parametry, można podać jako elementy tej struktury.

Można jeszcze bardziej oddzielić część, inicjalizacji od bibliotek sprzętowych. Wykonuje się to przez stworzenie własnego typu danych LL_GPIO_InitTypedef oraz utworzenie własnej funkcji inicjalizacyjnej. Biblioteki LL mają znaczniej mniej dodatkowych instrukcji w funkcjach niż biblioteki HAL, przez co ta operacja będzie bardziej uproszczona. 

Można także stworzyć sobie tablicę konfiguracyjną, z pinami wyjściowymi lub wszystkimi jakie są obsługiwane w urządzeniu: 

  1. typedef struct {
  2.       GPIO_TypeDef *Port;
  3.       uint32_t Pin;
  4.       uint32_t ClockMsk;
  5.       uint32_t Mode;
  6.       uint32_t Speed;
  7.       uint32_t OutputType;
  8.       uint32_t Pull;
  9.       uint32_t Alternate;
  10. }gpioConfig_t;
  11.  
  12. static gpioConfig_t const gpioConfig[] =
  13. {
  14.     { PK1_PORT, PK1_PIN, PK1_CLOCK_MASK, LL_GPIO_MODE_OUTPUT, LL_GPIO_SPEED_FREQ_HIGH, LL_GPIO_OUTPUT_PUSHPULL, LL_GPIO_PULL_NO},
  15.     { PK2_PORT, PK2_PIN, PK2_CLOCK_MASK, LL_GPIO_MODE_OUTPUT, LL_GPIO_SPEED_FREQ_HIGH, LL_GPIO_OUTPUT_PUSHPULL, LL_GPIO_PULL_NO},
  16.     { PK3_PORT, PK3_PIN, PK3_CLOCK_MASK, LL_GPIO_MODE_OUTPUT, LL_GPIO_SPEED_FREQ_HIGH, LL_GPIO_OUTPUT_PUSHPULL, LL_GPIO_PULL_NO},
  17.     { PK4_PORT, PK4_PIN, PK4_CLOCK_MASK, LL_GPIO_MODE_OUTPUT, LL_GPIO_SPEED_FREQ_HIGH, LL_GPIO_OUTPUT_PUSHPULL, LL_GPIO_PULL_NO},
  18.     { PK5_PORT, PK5_PIN, PK5_CLOCK_MASK, LL_GPIO_MODE_OUTPUT, LL_GPIO_SPEED_FREQ_HIGH, LL_GPIO_OUTPUT_PUSHPULL, LL_GPIO_PULL_NO},
  19.     { PK6_PORT, PK6_PIN, PK6_CLOCK_MASK, LL_GPIO_MODE_OUTPUT, LL_GPIO_SPEED_FREQ_HIGH, LL_GPIO_OUTPUT_PUSHPULL, LL_GPIO_PULL_NO},
  20. };

Dzięki temu uzyskujemy pełną tablicę konfiguracyjną, dla wszystkich wyprowadzeń w układzie. Co może być przydatne w przypadku konieczności sterowania dużą ilością pinów.  

Pozostałe funkcje odpowiedzialne za sterowanie:

  1. uint8_t ReadRelayPins(GPIO_TypeDef *portPtr, const uint32_t pinMask) {
  2.     return ((READ_BIT(portPtr->ODR, pinMask) == (pinMask)) ? 1UL : 0UL);
  3. }
  4.  
  5. uint8_t WriteRelayPins(GPIO_TypeDef *portPtr, const uint32_t pinMask, const uint8_t state, const _Bool polar) {
  6.     if(state == 1) {
  7.         if(polar == true) {
  8.             WRITE_REG(portPtr->BSRR, pinMask << 16U);
  9.         }
  10.         else {
  11.             WRITE_REG(portPtr->BSRR, pinMask);
  12.         }
  13.         return 1;
  14.     } else {
  15.         if(polar == true)
  16.         {
  17.             WRITE_REG(portPtr->BSRR, pinMask);
  18.         } else {
  19.             WRITE_REG(portPtr->BSRR, pinMask << 16U);
  20.         }
  21.     }
  22.     return 0;
  23. }
  24.  
  25. uint8_t ToggleRelayPins(GPIO_TypeDef *portPtr, const uint32_t pinMask) {
  26.     uint32_t odr = READ_REG(portPtr->ODR);
  27.     WRITE_REG(portPtr->BSRR, ((odr & pinMask) << 16u) | (~odr & pinMask));
  28.     return ((READ_BIT(portPtr->ODR, pinMask) == (pinMask)) ? 1UL : 0UL);
  29. }

W celu wywołania funkcji należy posłużyć się następującą instrukcją:

  1. static uint8_t EnablePKx(const uint8_t pkNumber, const _Bool polar)
  2. {
  3.     if (pkNumber >= 1 && pkNumber <= 6)
  4.     {
  5.         KonfigKanalWyjscie[pkNumber - 1].KonfigPrzekaznik.Licznik = 1;
  6.         Zdarzenia_ZapisZdarzeniaZmianaStanuWyjsciaWBuforze(pkNumber, KonfigKanalWyjscie[pkNumber - 1].Funkcja, 1);
  7.         return relay_interface_get()->write(KonfigKanalWyjscie[pkNumber - 1].Port, KonfigKanalWyjscie[pkNumber - 1].Pin, 1, polar);
  8.     }
  9.  
  10.     return 0xFF;
  11. }

Dzięki takiemu oddzieleniu funkcji bezpośrednio sterujących sprzętem od reszty programu uzyskujemy więcej przejrzystości w głównej logice programu. Dodatkowo zapewnia to łatwiejsze przeprowadzanie testów, ponieważ w celu zasymulowania sterowania musimy jedynie zmodyfikować wskaźniki na funkcje zapisane w interfejsie.