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:
- typedef struct {
- int16_t Rotation_Value;
- uint8_t Last_Pin_A_Status;
- int16_t Encoder_Counter;
- GPIO_TypeDef* GPIO_A;
- GPIO_TypeDef* GPIO_B;
- uint16_t GPIO_PIN_A;
- uint16_t GPIO_PIN_B;
- }ENCODER_t;
Poniżej prototypy trzech funkcji:
- void Set_Basic_Data_Struct(ENCODER_t* Encoder_t)
- void Encoder_Init_Module(ENCODER_t* Encoder_t);
- 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.
- void Set_Basic_Data_Struct(ENCODER_t* Encoder_t)
- {
- //Wprowadz dane do struktury
- Encoder_t->GPIO_A = GPIOD;
- Encoder_t->GPIO_B = GPIOD;
- Encoder_t->GPIO_PIN_A = GPIO_Pin_0;
- Encoder_t->GPIO_PIN_B = GPIO_Pin_1;
- Encoder_t->Encoder_Counter = 0;
- Encoder_t->Rotation_Value = 0;
- Encoder_t->Last_Pin_A_Status = 1;
- }
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.
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.
- void Encoder_Init_Module(ENCODER_t* struct_data)
- {
- GPIO_InitTypeDef GPIO_In;
- EXTI_InitTypeDef EXTI_In;
- NVIC_InitTypeDef NVIC_In;
- RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);
- RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
- //Ustawienie pinu oraz przypisanie przerwania
- GPIO_In.GPIO_Mode = GPIO_Mode_IN;
- GPIO_In.GPIO_OType = GPIO_OType_PP;
- GPIO_In.GPIO_Pin = Encoder_t->GPIO_PIN_A;
- GPIO_In.GPIO_PuPd = GPIO_PuPd_UP;
- GPIO_In.GPIO_Speed = GPIO_Speed_100MHz;
- GPIO_Init(Encoder_t->GPIO_A, &GPIO_In);
- SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOD, EXTI_PinSource0);
- EXTI_In.EXTI_Line = EXTI_Line0;
- EXTI_In.EXTI_LineCmd = ENABLE;
- EXTI_In.EXTI_Mode = EXTI_Mode_Interrupt;
- EXTI_In.EXTI_Trigger = EXTI_Trigger_Rising_Falling;
- EXTI_Init(&EXTI_In);
- NVIC_In.NVIC_IRQChannel = EXTI0_IRQn;
- NVIC_In.NVIC_IRQChannelPreemptionPriority = 0x00;
- NVIC_In.NVIC_IRQChannelSubPriority = 0x00;
- NVIC_In.NVIC_IRQChannelCmd = ENABLE;
- NVIC_Init(&NVIC_In);
- //Drugi z pinów jest ustawiany jako wyjscie
- GPIO_In.GPIO_Pin = Encoder_t->GPIO_PIN_B;
- GPIO_In.GPIO_PuPd = GPIO_PuPd_UP;
- GPIO_In.GPIO_OType = GPIO_OType_PP;
- GPIO_In.GPIO_Mode = GPIO_Mode_IN;
- GPIO_In.GPIO_Speed = GPIO_Speed_25MHz;
- GPIO_Init(Encoder_t->GPIO_B, &GPIO_In);
- }
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.
- void Encoder_Get_Rotation_Value(ENCODER_t* struct_data)
- {
- uint8_t actual_a;
- uint8_t actual_b;
- //Odczytaj dane
- actual_a = (((struct_data->GPIO_A)->IDR & (struct_data->GPIO_PIN_A)) == 0 ? 0 : 1);
- actual_b = (((struct_data->GPIO_B)->IDR & (struct_data->GPIO_PIN_B)) == 0 ? 0 : 1);
- //Sprawdzenie stanu, czy inna wartosc na linii
- if (actual_a != struct_data->Last_Pin_A_Status)
- {
- //Jesli tak to przypisz nowa
- struct_data->Last_Pin_A_Status = actual_a;
- if (struct_data->Last_Pin_A_Status == 0)
- {
- if (actual_b == 1)
- {
- if(struct_data->Encoder_Counter == 24)
- {
- struct_data->Encoder_Counter= 24;
- }
- else
- {
- struct_data->Encoder_Counter++;
- }
- }
- }
- else
- {
- if (actual_b == 1)
- {
- if(struct_data->Encoder_Counter== 0)
- {
- struct_data->Encoder_Counter= 0;
- }
- else
- {
- struct_data->Encoder_Counter--;
- }
- }
- }
- }
- }
Teraz czas na obsługę przerwania:
- void EXTI0_IRQHandler()
- {
- if (EXTI_GetITStatus(EXTI_Line0) != RESET)
- {
- EXTI_ClearITPendingBit(EXTI_Line0);
- Encoder_Get_Rotation_Value(&Encoder_t);
- }
- }
Poniżej wklejam jeszcze część do włączenia UART-u funkcję main dla programu:
- struct __FILE {
- int parametr;
- };
- //Stworzenie zmiennej z struktury FILE
- //Parametr musi miec taka nazwe
- FILE __stdout;
- void USART_Initialize(void)
- {
- //Inicjalizacja kontrolera przerwan
- NVIC_InitTypeDef NVIC_InitStruct;
- //konfiguracja ukladu USART
- USART_InitTypeDef USART_InitStructure;
- //Ustawienie kanalu IRQ
- NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
- //Wlaczenie zegara dla USART1
- RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
- //Ustawienie predkosci transmisji 9600bps
- USART_InitStructure.USART_BaudRate = 9600;
- //Dlugosc wysylanego slowa
- USART_InitStructure.USART_WordLength = USART_WordLength_8b;
- //Ustawienie jednego bitu stopu
- USART_InitStructure.USART_StopBits = USART_StopBits_1;
- //Kontrola parzystosci wylaczona
- USART_InitStructure.USART_Parity = USART_Parity_No;
- //Wylaczenie kontroli przeplywu danych
- USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
- //Tryb pracy linii odpowiednio odbior i nadawanie
- USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
- //Konfiguracja ukladu
- USART_Init(USART1, &USART_InitStructure);
- //Wlaczenie USART1
- USART_Cmd(USART1, ENABLE);
- //Wlaczenie przerwania na RX1
- USART1->CR1 |= USART_CR1_RXNEIE;
- //Wprowadzenie ustawien do przerwan
- NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
- NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
- NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
- NVIC_Init(&NVIC_InitStruct);
- }
- void GPIO_Initialize(void)
- {
- //konfigurowanie portow GPIO
- GPIO_InitTypeDef GPIO_In;
- RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
- //TX dla pinu PB6
- GPIO_In.GPIO_Pin = GPIO_Pin_6;
- GPIO_In.GPIO_PuPd = GPIO_PuPd_UP;
- GPIO_In.GPIO_OType = GPIO_OType_PP;
- GPIO_In.GPIO_Mode = GPIO_Mode_AF;
- GPIO_In.GPIO_Speed = GPIO_Speed_100MHz;
- GPIO_Init(GPIOB, &GPIO_In);
- //RX dla pinu PB7
- GPIO_In.GPIO_Pin = GPIO_Pin_7;
- GPIO_In.GPIO_Mode = GPIO_Mode_AF;
- GPIO_In.GPIO_PuPd = GPIO_PuPd_UP;
- GPIO_In.GPIO_OType = GPIO_OType_PP;
- GPIO_In.GPIO_Speed = GPIO_Speed_100MHz;
- GPIO_Init(GPIOB, &GPIO_In);
- //Wlaczenie transmisji na podanych pinach
- GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_USART1);
- GPIO_PinAFConfig(GPIOB, GPIO_PinSource7, GPIO_AF_USART1);
- }
- void USART_Send(volatile char *c)
- {
- //Petla dziala do puki bedzie jakis znak do wyslania
- while(*c)
- {
- //Sprawdza czy rejestr danych zostal oprózniony
- while( !(USART1->SR & 0x00000040) );
- //Przeslij dane,
- USART_SendData(USART1, *c);
- *c++;
- }
- }
- //Funkcja wysylajaca dane do strumienia
- //Jej nazwy nie mozna zmieniac
- int fputc(int ch, FILE *f)
- {
- volatile char c = ch;
- //Wyslanie danych
- USART1->DR = (uint16_t)(c & 0x01FF);
- //Odczekanie az bufor zostanie oprozniony
- while (!((USART1)->SR & USART_FLAG_TXE))
- {}
- return ch;
- }
- void Send_Charc(volatile char c)
- {
- while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
- USART_SendData(USART1, c);
- }
- int main(void) {
- SystemInit();
- //UART
- GPIO_Initialize();
- USART_Initialize();
- //Enkoder
- Set_Basic_Data_Struct(&Encoder_t);
- Encoder_Init_Module(&Encoder_t);
- while (1)
- {
- printf("War: %d\r\n", ENCODER_t.Rotation_Value);
- delay_ms(1000);
- }
- }
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ł.
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ł.