czwartek, 27 października 2016

[23] STM32F4 - Obsługa enkodera inkrementalnego

W tym poście chciałbym opisać w jaki sposób obsłużyć enkoder inkrementalny. W przykładzie wykorzystałem enkoder, który można znaleźć pod tym linkiem oraz drugi dla jakiego sprawdzałem czyli ten. Oba mają po 24 kroki na pełny obrót.

Podłączenie


Spotkałem się z czterema metodami podłączenia enkodera do mikrokontrolera.

Pierwsze podłączenie dotyczy podpięcia dwóch rezystorów o wartości 10k do stanu wysokiego. Często jest on stosowany w dokumentacji od enkodera z różnymi rezystorami 10k lub 5k.


Drugi sposób dodatkowo zawiera kondensatory o wartości 100nF, Jest to najprostszy rekomendowany sposób. Natomiast mogą wystąpić pewne problemy dotyczące czasu narostu zboczy oraz poziomu tego sygnału.


Czas na trzeci sposób który zaobserwowałem na blogu Atnel. W nim zmienione są wartości kondensatorów na 10nF oraz dodano rezystory wpięte szeregowo w układ. Ja korzystałem z takiej konfiguracji przy jednym z projektów:


Poniżej jeszcze jeden sposób dotyczący podłączenia, tym razem zaczerpnięty z dokumentacji producenta wykorzystywanych enkoderów. Jest to właściwie niewielka modyfikacja powyższego schematu:



Programowanie


Aby poprawnie obsłużyć enkoder wykorzystałem dwa piny mikrokontrolera, z czego jeden z nich generuje przerwania.

Na początku rozpocznę od pliku nagłówkowego, w którym została zdefiniowana potrzebna struktury dla danych z enkodera oraz deklaracje funkcji:

  1. typedef struct {
  2.     int16_t Rotation_Value;
  3.     uint8_t Last_Pin_A_Status;
  4.     int16_t Encoder_Counter;
  5.     GPIO_TypeDef* GPIO_A;
  6.     GPIO_TypeDef* GPIO_B;
  7.     uint16_t GPIO_PIN_A;
  8.     uint16_t GPIO_PIN_B;
  9. }ENCODER_t;

Poniżej prototypy trzech funkcji:

  1. void Set_Basic_Data_Struct(ENCODER_t* Encoder_t)
  2. void Encoder_Init_Module(ENCODER_t* Encoder_t);
  3. void Encoder_Get_Rotation_Value(ENCODER_t* Encoder_t));

Pierwsza funkcja ustawia domyślne wartości dla struktury, druga natomiast pozwala na włączenie EXTI oraz GPIO. Trzecia jest obsługiwana w przerwaniu, sprawdza stan przycisku i określa kierunek obrotu.

  1. void Set_Basic_Data_Struct(ENCODER_t* Encoder_t)
  2. {
  3.     //Wprowadz dane do struktury
  4.     Encoder_t->GPIO_A = GPIOD;
  5.     Encoder_t->GPIO_B = GPIOD;
  6.     Encoder_t->GPIO_PIN_A = GPIO_Pin_0;
  7.     Encoder_t->GPIO_PIN_B = GPIO_Pin_1;
  8.    
  9.     Encoder_t->Encoder_Counter = 0;
  10.     Encoder_t->Rotation_Value = 0;
  11.     Encoder_t->Last_Pin_A_Status = 1;
  12. }

Następnie wspomniane wcześniej funkcja włącza piny, przypisuje jednemu z nich przerwanie, drugi natomiast jest ustawiony jako wejście. Nie ma sensu ustawiania przerwania na obu zboczach, ponieważ i tak podczas pracy z enkoderem oba zostaną wyzwolone, bez względu na to w którą stronę nastąpi obrót.

Pierwsza funkcja ma za zadanie włączenie enkodera, czyli jego wyprowadzeń, oraz zegarów. Układ został podłączony do pinów PD0 oraz PD1. 

  1. void Encoder_Init_Module(ENCODER_t* struct_data)
  2. {
  3.   GPIO_InitTypeDef GPIO_In;
  4.   EXTI_InitTypeDef EXTI_In;
  5.   NVIC_InitTypeDef NVIC_In;
  6.    
  7.   RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);
  8.   RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
  9.    
  10.   //Ustawienie pinu oraz przypisanie przerwania
  11.   GPIO_In.GPIO_Mode = GPIO_Mode_IN;
  12.   GPIO_In.GPIO_OType = GPIO_OType_PP;
  13.   GPIO_In.GPIO_Pin = Encoder_t->GPIO_PIN_A;
  14.   GPIO_In.GPIO_PuPd = GPIO_PuPd_UP;
  15.   GPIO_In.GPIO_Speed = GPIO_Speed_100MHz;
  16.   GPIO_Init(Encoder_t->GPIO_A, &GPIO_In);
  17.    
  18.   SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOD, EXTI_PinSource0);
  19.   EXTI_In.EXTI_Line = EXTI_Line0;
  20.   EXTI_In.EXTI_LineCmd = ENABLE;
  21.   EXTI_In.EXTI_Mode = EXTI_Mode_Interrupt;
  22.   EXTI_In.EXTI_Trigger = EXTI_Trigger_Rising_Falling;
  23.   EXTI_Init(&EXTI_In);
  24.   NVIC_In.NVIC_IRQChannel = EXTI0_IRQn;
  25.   NVIC_In.NVIC_IRQChannelPreemptionPriority = 0x00;
  26.   NVIC_In.NVIC_IRQChannelSubPriority = 0x00;
  27.   NVIC_In.NVIC_IRQChannelCmd = ENABLE;
  28.   NVIC_Init(&NVIC_In);
  29.    
  30.   //Drugi z pinów jest ustawiany jako wyjscie
  31.   GPIO_In.GPIO_Pin = Encoder_t->GPIO_PIN_B;
  32.   GPIO_In.GPIO_PuPd = GPIO_PuPd_UP;
  33.   GPIO_In.GPIO_OType = GPIO_OType_PP;
  34.   GPIO_In.GPIO_Mode = GPIO_Mode_IN;
  35.   GPIO_In.GPIO_Speed = GPIO_Speed_25MHz;
  36.   GPIO_Init(Encoder_t->GPIO_B, &GPIO_In);
  37. }

Poniższa funkcja ma za zadanie sprawdzenie wartości na enkoderze. Na samym początku do zmiennych wprowadzane są stany z obu pinów. Następnie sprawdzane jest czy stan na przycisku pierwszym się zmienił. jeśli tak na następuje sprawdzenie warunków na podstawie nowej wartości ostatniego stany przycisku. W instrukcjach warunkowych można zmienić wartości maksymalnego odczytu z enkodera, w tym momencie jest on ustawiony na wartość 24, czyli po pełnym obrocie enkodera jest on zerowany.

  1. void Encoder_Get_Rotation_Value(ENCODER_t* struct_data)
  2. {
  3.     uint8_t actual_a;
  4.     uint8_t actual_b;
  5.    
  6.     //Odczytaj dane
  7.     actual_a = (((struct_data->GPIO_A)->IDR & (struct_data->GPIO_PIN_A)) == 0 ? 0 : 1);
  8.     actual_b = (((struct_data->GPIO_B)->IDR & (struct_data->GPIO_PIN_B)) == 0 ? 0 : 1);
  9.    
  10.     //Sprawdzenie stanu, czy inna wartosc na linii
  11.     if (actual_a != struct_data->Last_Pin_A_Status)
  12.     {
  13.         //Jesli tak to przypisz nowa
  14.         struct_data->Last_Pin_A_Status = actual_a;
  15.         if (struct_data->Last_Pin_A_Status == 0)
  16.         {
  17.             if (actual_b == 1)
  18.                 {
  19.                     if(struct_data->Encoder_Counter == 24)
  20.                     {
  21.                         struct_data->Encoder_Counter= 24;
  22.                     }
  23.                     else
  24.                     {
  25.                         struct_data->Encoder_Counter++;
  26.                     }
  27.                 }
  28.             }
  29.             else
  30.             {
  31.                 if (actual_b == 1)
  32.                 {
  33.                     if(struct_data->Encoder_Counter== 0)
  34.                     {
  35.                         struct_data->Encoder_Counter= 0;
  36.                     }
  37.                     else
  38.                     {
  39.                         struct_data->Encoder_Counter--;
  40.                     }
  41.                 }
  42.         }
  43.     }
  44. }

Teraz czas na obsługę przerwania:

  1. void EXTI0_IRQHandler()
  2. {
  3.     if (EXTI_GetITStatus(EXTI_Line0) != RESET)
  4.     {
  5.         EXTI_ClearITPendingBit(EXTI_Line0);
  6.         Encoder_Get_Rotation_Value(&Encoder_t);
  7.     }
  8. }

Poniżej wklejam jeszcze część do włączenia UART-u funkcję main dla programu:

  1. struct __FILE {
  2.     int parametr;
  3. };
  4. //Stworzenie zmiennej z struktury FILE
  5. //Parametr musi miec taka nazwe
  6. FILE __stdout;
  7. void USART_Initialize(void)
  8. {
  9.      //Inicjalizacja kontrolera przerwan
  10.      NVIC_InitTypeDef NVIC_InitStruct;
  11.    
  12.      //konfiguracja ukladu USART
  13.      USART_InitTypeDef USART_InitStructure;
  14.      
  15.      //Ustawienie kanalu IRQ
  16.      NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
  17.    
  18.      //Wlaczenie zegara dla USART1
  19.      RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
  20.      
  21.      //Ustawienie predkosci transmisji 9600bps
  22.      USART_InitStructure.USART_BaudRate = 9600;
  23.      //Dlugosc wysylanego slowa
  24.      USART_InitStructure.USART_WordLength = USART_WordLength_8b;
  25.      //Ustawienie jednego bitu stopu
  26.      USART_InitStructure.USART_StopBits = USART_StopBits_1;
  27.      //Kontrola parzystosci wylaczona
  28.      USART_InitStructure.USART_Parity = USART_Parity_No;
  29.      //Wylaczenie kontroli przeplywu danych
  30.      USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
  31.      //Tryb pracy linii odpowiednio odbior i nadawanie
  32.      USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
  33.      
  34.      //Konfiguracja ukladu
  35.      USART_Init(USART1, &USART_InitStructure);
  36.      //Wlaczenie USART1
  37.      USART_Cmd(USART1, ENABLE);
  38.      
  39.      //Wlaczenie przerwania na RX1
  40.      USART1->CR1 |= USART_CR1_RXNEIE;
  41.    
  42.      //Wprowadzenie ustawien do przerwan
  43.      NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
  44.      NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
  45.      NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
  46.      NVIC_Init(&NVIC_InitStruct);
  47. }
  48. void GPIO_Initialize(void)
  49. {
  50.      //konfigurowanie portow GPIO
  51.      GPIO_InitTypeDef  GPIO_In;
  52.      RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
  53.      
  54.      //TX dla pinu PB6
  55.      GPIO_In.GPIO_Pin = GPIO_Pin_6;
  56.      GPIO_In.GPIO_PuPd = GPIO_PuPd_UP;
  57.      GPIO_In.GPIO_OType = GPIO_OType_PP;
  58.      GPIO_In.GPIO_Mode = GPIO_Mode_AF;
  59.      GPIO_In.GPIO_Speed = GPIO_Speed_100MHz;
  60.      GPIO_Init(GPIOB, &GPIO_In);
  61.      
  62.      //RX dla pinu PB7
  63.      GPIO_In.GPIO_Pin = GPIO_Pin_7;
  64.      GPIO_In.GPIO_Mode = GPIO_Mode_AF;
  65.      GPIO_In.GPIO_PuPd = GPIO_PuPd_UP;
  66.      GPIO_In.GPIO_OType = GPIO_OType_PP;
  67.      GPIO_In.GPIO_Speed = GPIO_Speed_100MHz;
  68.      GPIO_Init(GPIOB, &GPIO_In);
  69.      
  70.      //Wlaczenie transmisji na podanych pinach
  71.      GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_USART1);
  72.      GPIO_PinAFConfig(GPIOB, GPIO_PinSource7, GPIO_AF_USART1);
  73. }
  74. void USART_Send(volatile char *c)
  75. {
  76.      //Petla dziala do puki bedzie jakis znak do wyslania
  77.      while(*c)
  78.      {
  79.         //Sprawdza czy rejestr danych zostal oprózniony
  80.         while( !(USART1->SR & 0x00000040) );
  81.         //Przeslij dane,
  82.         USART_SendData(USART1, *c);
  83.         *c++;
  84.      }
  85. }
  86. //Funkcja wysylajaca dane do strumienia
  87. //Jej nazwy nie mozna zmieniac
  88. int fputc(int ch, FILE *f)
  89. {
  90.     volatile char c = ch;
  91.    
  92.       //Wyslanie danych
  93.         USART1->DR = (uint16_t)(& 0x01FF);
  94.         //Odczekanie az bufor zostanie oprozniony
  95.       while (!((USART1)->SR & USART_FLAG_TXE))
  96.         {}
  97.        
  98.     return ch;
  99. }
  100. void Send_Charc(volatile char c)
  101. {
  102.      while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
  103.      USART_SendData(USART1, c);
  104. }

  1. int main(void) {
  2.     SystemInit();
  3.     //UART
  4.     GPIO_Initialize();
  5.     USART_Initialize();
  6.     //Enkoder
  7.     Set_Basic_Data_Struct(&Encoder_t);
  8.     Encoder_Init_Module(&Encoder_t);
  9.     while (1)
  10.     {
  11.         printf("War: %d\r\n", ENCODER_t.Rotation_Value);
  12.         delay_ms(1000);
  13.     }
  14. }

Na koniec chciałbym jeszcze dodać, że im droższy enkoder tym prawdopodobnie mniej czasu zajmie odpowiednie przygotowanie oprogramowania. Dodatkowo przyjemność z użytkowania będzie znacznie wyższa.

Głównie chodzi o występujący "klik" pomiędzy przejściami na kolejny krok. Dla enkodera wspomnianego na początku kliknięcia pomiędzy pozycjami są dosyć mocno zaznaczone. Dla drugiego enkodera który testowałem(czyli tego) można wyczuć co drugi krok. Czyli zmiana stanu odbywa się na nim co pół kroku. No ale coś za coś pierwszy kosztuje około 18 zł, drugi około 5 zł.