23 października 2018

GPIO ciąg dalszy

Obiecałem wrócić do linii wejścia-wyjścia, więc wracam. Na początek wyjaśnienie o co chodziło z rejestrem ODR. Najlepiej będzie wyjaśnić problem na przykładzie - tylko zamiast migania diodami użyję analizatora stanów logicznych. Z diodami byłoby dokładnie tak samo, ale trudniej zauważyć efekt. A diagram powinien być wart tysiąc słów.
Na początek to co było, czyli program generujący przebieg prostokątny. Używam już taktowania 48MHz, inicjalizację zegara przeniosłem do nowej funkcji clock_init() - zmieniłem też pin używany do testu. Wybrałem PC8, bo jest łatwo dostępny. Później będę jeszcze używał PC6, który jest wyprowadzony zaraz obok. To co ważne to użycie dwóch pinów z tego samego portu. No i oczywiście wygoda łączenia kabelków.
Pierwsza wersja programu:

#include "stm32f0xx.h"

void clock_init(void)
{
    RCC->CFGR = RCC_CFGR_PLLSRC_HSI_DIV2 | RCC_CFGR_PLLMUL12;
 RCC->CR |= RCC_CR_PLLON;
    while ((RCC->CR & RCC_CR_PLLRDY) == 0) ;

 FLASH->ACR |= FLASH_ACR_LATENCY;

    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS_PLL) != RCC_CFGR_SWS_PLL) ;
}

int main(void)
{
 clock_init();

 RCC->AHBENR |= RCC_AHBENR_GPIOAEN | RCC_AHBENR_GPIOCEN;

 GPIOC->MODER |= GPIO_MODER_MODER8_0 | GPIO_MODER_MODER6_0;

 while (1)
 {
  GPIOC->ODR |= GPIO_ODR_8;
  for (volatile uint32_t dly = 0; dly < 1000; dly++);

  GPIOC->ODR &= ~GPIO_ODR_8;
  for (volatile uint32_t dly = 0; dly < 1000; dly++);
 }
} 

Program oczywiście testuję, przebieg sygnału wygląda zgodnie z oczekiwaniami:


Teraz potrzebne będzie przerwanie. Procesory z rdzeniem Cortex-M mają wbudowany licznik SysTick najczęściej używany przez systemy operacyjne. Jest to bardzo prosty w użyciu moduł, możemy go więc wykorzystać do sterowania pinu PC6.
Najpierw piszemy funkcję obsługi przerwania - jest to zwykła funkcja o nazwie SysTick_Handler. W niej będziemy zmieniać stan pinu PC6 na przeciwny. Kod ładniej wyglądałby z użyciem operatora alternatywy wykluczającej (XOR), ale chciałem zostać przy znanym kodzie.
Cała konfiguracja timera sprowadza się do wywołania funkcji SysTick_Config. Jako parametr podajemy liczbę taktów zegara systemowego między przerwaniami. Ponieważ częstotliwość taktowania to 48MHz, podaję 48000 aby uzyskać przerwania co 1ms.
Na początek komentuję treść pętli głównej i sprawdzam, czy program działa:

#include "stm32f0xx.h"

void clock_init(void)
{
    RCC->CFGR = RCC_CFGR_PLLSRC_HSI_DIV2 | RCC_CFGR_PLLMUL12;
 RCC->CR |= RCC_CR_PLLON;
    while ((RCC->CR & RCC_CR_PLLRDY) == 0) ;

 FLASH->ACR |= FLASH_ACR_LATENCY;

    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS_PLL) != RCC_CFGR_SWS_PLL) ;
}

void SysTick_Handler(void)
{
 static int output_on = 0;

 output_on = !output_on;

 if (output_on)
  GPIOC->ODR |= GPIO_ODR_6;
 else
  GPIOC->ODR &= ~GPIO_ODR_6;
}

int main(void)
{
 clock_init();

 RCC->AHBENR |= RCC_AHBENR_GPIOAEN | RCC_AHBENR_GPIOCEN;

 GPIOC->MODER |= GPIO_MODER_MODER8_0 | GPIO_MODER_MODER6_0;

 SysTick_Config(48000);

 while (1)
 {
  /*
  GPIOC->ODR |= GPIO_ODR_8;
  for (volatile uint32_t dly = 0; dly < 1000; dly++);

  GPIOC->ODR &= ~GPIO_ODR_8;
  for (volatile uint32_t dly = 0; dly < 1000; dly++);
  */
 }
}

Na analizatorze używam już dwóch kanałów, pierwszy jest podłączony do pinu PC8, drugi do właśnie testowanego PC6.

Wszystko wygląda pięknie, czas coś popsuć. Usuwam więc komentarz z pętli głównej programu i uruchamiam analizator logiczny. Okazało się, że wcale nie tak łatwo błąd wytropić, ale w końcu dobrałem wyzwalanie i wyszło coś takiego:

W środkowej części widzimy zakłócenie na linii PC6. Przerwanie od SysTick pojawiło się dokładnie w momencie, gdy główny program był między pobraniem zawartości rejestru ODR, a zapisaniem zmienionej wersji. Kod przerwania zmienił zawartość rejestru ODR, ale chwilę później program główny nadpisał tą zmianę.
Takie działanie może wyglądać na mało prawdopodobne i nieszkodliwe. ale to czasem bardzo poważny błąd. Po pierwsze trudny do wykrycia - występuje tylko w pechowych chwilach, gdy przerwanie pojawi się w nieodpowiednim momencie. Przykładowy program i tak co 1ms zmieniał stan PC6, więc błąd został po chwili naprawiony - ale co jeśli to przerwanie ustawiałoby tylko raz i to ważny moduł...

Na szczęście używanie rejestru ODR to nie jedyna możliwość. Są jeszcze dwa rejestry BSRR pozwala na ustawianie oraz zerowanie stanu pinów w sposób atomowy, a BRR tylko na zerowanie.

Jak widzimy dolne 16 bitów rejestru BSRR nazywane jest BSx, gdzie x to numer pinu - służą one do ustawiania stanu wysokiego na odpowiednim wyjściu. Analogicznie wyższe bity zostały nazwane BRx i służą do ustawiania stanu niskiego.
Rejestr BRR ma tylko bity BRx:
Jego funkcjonalność duplikuje się z BSRR, ale wygodne jest to że te same bity w BSRR ustawiają stan wysoki, co stan niski w BRR. Niektóre modele STM32 nie mają rejestru BRR, ale BSRR pojawia się właściwie zawsze.
Najważniejsza różnica to ignorowanie zapisu zer do rejestrów BSRR oraz BRR. Zapis jedynki ustawia lub zeruje odpowiednie wyprowadzenie, natomiast zero nic nie zmienia. Dzięki temu wystarczy zwykły zapis (=) do rejestru, zamiast operacji alternatywy (|=), czy koniunkcji (&=). Ponieważ zapisy są atomowe, więc w trakcie ich wykonywania nie może pojawić się przerwanie.
Zmienię teraz program, tak aby używał rejestrów BSRR oraz BRR zamiast ODR:


#include "stm32f0xx.h"

void clock_init(void)
{
    RCC->CFGR = RCC_CFGR_PLLSRC_HSI_DIV2 | RCC_CFGR_PLLMUL12;
 RCC->CR |= RCC_CR_PLLON;
    while ((RCC->CR & RCC_CR_PLLRDY) == 0) ;

 FLASH->ACR |= FLASH_ACR_LATENCY;

    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS_PLL) != RCC_CFGR_SWS_PLL) ;
}

void SysTick_Handler(void)
{
 static int output_on = 0;

 output_on = !output_on;

 if (output_on)
  GPIOC->BSRR = GPIO_BSRR_BS_6;
 else
  GPIOC->BSRR = GPIO_BSRR_BR_6;
}

int main(void)
{
 clock_init();

 RCC->AHBENR |= RCC_AHBENR_GPIOAEN | RCC_AHBENR_GPIOCEN;

 GPIOC->MODER |= GPIO_MODER_MODER8_0 | GPIO_MODER_MODER6_0;

 SysTick_Config(48000);

 while (1)
 {
  GPIOC->BSRR = GPIO_BSRR_BS_8;
  for (volatile uint32_t dly = 0; dly < 1000; dly++);

  GPIOC->BSRR = GPIO_BSRR_BR_8;
  for (volatile uint32_t dly = 0; dly < 1000; dly++);
 }
}

Na koniec test dla upewnienia się, że wszystko działa jak powinno:



Od tej chwili będę się starał unikać używania ODR - łatwo w jego przypadku o trudne do wychwycenia błędy w programie.



Brak komentarzy:

Prześlij komentarz