More Pages

Friday, July 1, 2016

ARM Cortex-M3 (STM32F103) Tutorial - Incremental Rotary Encoder

Rotary encoder can be used for several application such as digital volume control, DC motor position sensor, etc. Rotary encoder is an electro-mechanical device that converts the angular position to an analog or digital code.


There are two type of rotary encoder: absolute and incremental. Absolute rotary encoder produce unique digital code for each distinct angle of the shaft. Incremental rotary encoder produce two square waves usually in quadrature output (channel A and B). The quadrature means that the output pulses of channel A and B are 90 degree out of phase.

In this tutorial, I will use incremental quadrature rotary encoder. With the quadrature rotary encoder, there are 3 methods for counting pulses. The different between these methods is on which edges of which channel are counted.
  • 1x resolution: in this method, either the rising or falling edge of one channel (channel A) is counted. To determine whether, the encoder is clockwise or counterclockwise is by reading the channel B value every rising edge of channel A. For example, when channel A is rising and channel B is low, then the encoder is considered clockwise (increment). When channel A is rising and channel B is high, then the encoder is considered counterclockwise (decrement). This method needs one external interrupt pin on microcontroller for channel A and one GPIO pin for channel B.
  • 2x resolution: in this method, both the rising and falling edges of one channel (channel A) are counted. To determine whether, the encoder is clockwise or counterclockwise is by reading the channel A and B value every rising and falling edge of channel A. The encoder is considered clockwise (increment) if the value of channel A and B (after rising and falling edge of channel A) is different. The encoder is considered counterclockwise (decrement) if the value of channel A and B (after rising and falling edge of channel A) is same. This method needs one external interrupt pin on microcontroller for channel A and one GPIO pin for channel B.
  • 4x resolution: in this method, both the rising and falling edges of both channels A and B are counted. To determine whether, the encoder is clockwise or counterclockwise is by comparing the channel A value with the last value (90 degree lag) of channel B every rising and falling edge of both channel. For example, when channel A is rising, the value of A is high and the value of last B is low. Next, when channel B is rising, the value of A is high and the value of last B is low. Next, when channel A is falling, the value of A is low and the value of last B is high. Finally, when channel B is falling, the value of A is low and the value of last B is high. For all four falling and rising conditions, the value of channel A and the last value of channel B is always different, therefore we can considered the encoder clockwise (increment). For the counterclockwise direction, the value of channel A and the last value of channel B is always same. This method needs two external interrupt pin on microcontroller for channel A and channel B.

This is the circuit for rotary encoder. The capacitors is needed for debouncing.


This is example code for read rotary encoder using 1x resolution method:
#include "stm32f10x.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_exti.h"
#include "delay.h"
#include "lcd16x2.h"
#include <stdio.h>

// LCD custom char
uint8_t bar[][8] = 
{ 
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
    { 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10 },
    { 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18 },
    { 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C },
    { 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E },
    { 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F }
};
volatile int enc_cnt;
char enc_cnt_buf[4];

void init_lcd(void);
void init_rotary_encoder(void);
void lcd_update(void);
void rotary_encoder_update(void);

void EXTI9_5_IRQHandler(void)
{ 
    if (EXTI_GetITStatus(EXTI_Line6))
    {
        rotary_encoder_update();

        // Clear interrupt flag
        EXTI_ClearITPendingBit(EXTI_Line6);
    }
}

int main(void)
{
    DelayInit();
    init_lcd();
    init_rotary_encoder();

    while (1)
    {
        lcd_update();
    }
}

void init_lcd()
{
    uint8_t i;

    // Initialize LCD
    lcd16x2_init(LCD16X2_DISPLAY_ON_CURSOR_OFF_BLINK_OFF);

    // Fill custom char
    for (i = 0; i < 8; i++)
    {
        lcd16x2_create_custom_char(i, bar[i]);
    }
}

void init_rotary_encoder()
{
    GPIO_InitTypeDef GPIO_InitStruct;
    EXTI_InitTypeDef EXTI_InitStruct;
    NVIC_InitTypeDef NVIC_InitStruct;

    // Step 1: Initialize GPIO as input for rotary encoder
    // PB6 (encoder pin A), PB5 (encoder pin B)
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_5;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    // Step 2: Initialize EXTI for PB6
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource6);
    EXTI_InitStruct.EXTI_Line = EXTI_Line6;
    EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling;
    EXTI_InitStruct.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStruct);

    // Step 3: Initialize NVIC for EXTI9_5 IRQ channel
    NVIC_InitStruct.NVIC_IRQChannel = EXTI9_5_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0x00;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0x00;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);
}

void lcd_update()
{
    uint8_t div_bar, mod_bar;
    uint8_t i;

    div_bar = enc_cnt / 5;
    mod_bar = enc_cnt % 5;
    sprintf(enc_cnt_buf, "%i", enc_cnt);

    lcd16x2_clrscr();
    for (i = 0; i < div_bar; i++)
    {
        lcd16x2_put_custom_char(i, 0, 5);
    }
    lcd16x2_put_custom_char(i, 0, mod_bar);
    lcd16x2_gotoxy(0, 1);
    lcd16x2_puts(enc_cnt_buf);

    DelayMs(250);
}

void rotary_encoder_update()
{
    // When falling edge interrupt triggered by pin A of rotary encoder, 
    // then read pin B of rotary encoder (1x resolution)
    if ((GPIOB->IDR & GPIO_Pin_5))
    {
        enc_cnt++;
    }
    else
    {
        enc_cnt--;
    }

    // Set max and min value
    if (enc_cnt > 80)
    {
        enc_cnt = 80;
    }
    if (enc_cnt < 0)
    {
        enc_cnt = 0;
    }
}
In this code, I use LCD 16x2 for displaying encoder count and bargraph indicator. For 4x resolution method, I also write the code:
#include "stm32f10x.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_exti.h"
#include "delay.h"
#include "lcd16x2.h"
#include <stdio.h>

// LCD custom char
uint8_t bar[][8] = 
{ 
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
    { 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10 },
    { 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18 },
    { 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C },
    { 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E },
    { 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F }
};
volatile uint8_t last_enc_val_a;
volatile uint8_t last_enc_val_b;
volatile int enc_cnt;
char enc_cnt_buf[8];

void init_lcd(void);
void init_rotary_encoder(void);
void lcd_update(void);
void rotary_encoder_update(void);

void EXTI9_5_IRQHandler(void)
{
    if (EXTI_GetITStatus(EXTI_Line6) | EXTI_GetITStatus(EXTI_Line5))
    {
        rotary_encoder_update();
 
        // Clear interrupt flag
        EXTI_ClearITPendingBit(EXTI_Line6);
        EXTI_ClearITPendingBit(EXTI_Line5);
    }
}

int main(void)
{
    DelayInit();
    init_lcd();
    init_rotary_encoder();

    while (1)
    {
        lcd_update();
    }
}

void init_lcd()
{
    uint8_t i;

    // Initialize LCD
    lcd16x2_init(LCD16X2_DISPLAY_ON_CURSOR_OFF_BLINK_OFF);

    // Fill custom char
    for (i = 0; i < 8; i++)
    {
        lcd16x2_create_custom_char(i, bar[i]);
    }
}

void init_rotary_encoder()
{
    GPIO_InitTypeDef GPIO_InitStruct;
    EXTI_InitTypeDef EXTI_InitStruct;
    NVIC_InitTypeDef NVIC_InitStruct;

    // Step 1: Initialize GPIO as input for rotary encoder
    // PB6 (encoder pin A), PB5 (encoder pin B)
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_5;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOB, &GPIO_InitStruct);
    
    // Step 2: Initialize EXTI for PB6 and PB5
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource6);
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource5);
    EXTI_InitStruct.EXTI_Line = EXTI_Line6 | EXTI_Line5;
    EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising_Falling;
    EXTI_InitStruct.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStruct);

    // Step 3: Initialize NVIC for EXTI9_5 IRQ channel
    NVIC_InitStruct.NVIC_IRQChannel = EXTI9_5_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0x00;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0x00;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);
}

void lcd_update()
{
    uint8_t div_bar, mod_bar;
    uint8_t i;

    div_bar = enc_cnt / 5;
    mod_bar = enc_cnt % 5;
    sprintf(enc_cnt_buf, "%i", enc_cnt);

    lcd16x2_clrscr();
    for (i = 0; i < div_bar; i++)
    {
        lcd16x2_put_custom_char(i, 0, 5);
    }
    lcd16x2_put_custom_char(i, 0, mod_bar);
    lcd16x2_gotoxy(0, 1);
    lcd16x2_puts(enc_cnt_buf);

    DelayMs(250);
}

void rotary_encoder_update()
{
    uint8_t enc_val_a, enc_val_b;
    uint8_t enc_inc, enc_dec;

    // Read pin A and pin B every rising and falling edge interrupt
    // from both pin (4x resolution)
    enc_val_a = (uint8_t) ((GPIOB->IDR & GPIO_Pin_6) >> 6);
    enc_val_b = (uint8_t) ((GPIOB->IDR & GPIO_Pin_5) >> 5);

    // Read encoder direction using xor logic
    enc_inc = enc_val_a ^ last_enc_val_b;
    enc_dec = enc_val_b ^ last_enc_val_a;

    // Decrement or increment counter
    if(enc_inc)
    {
        enc_cnt++;
    }
    if(enc_dec)
    {
        enc_cnt--;
    }

    // Store encoder value for next reading
    last_enc_val_a = enc_val_a;
    last_enc_val_b = enc_val_b;

    // Set max and min value
    if (enc_cnt > 80)
    {
        enc_cnt = 80;
    }
    if (enc_cnt < 0)
    {
        enc_cnt = 0;
    }
}
On STM32F103C8 microcontroller there is also built-in timer that can be used for reading quadrature encoder. This timer acts like a counter. By using these timer, we can count rotary encoder without using external interrupt. Therefore, the cpu will not interrupted every rising and falling edge of the quadrature signal just for incrementing or decrementing the counter. This is the code for quadrature encoder using timer encoder:
#include "stm32f10x.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_tim.h"
#include "delay.h"
#include "lcd16x2.h"
#include <stdio.h>

uint16_t enc_cnt;
char enc_cnt_buf[8];

void init_lcd(void);
void init_rotary_encoder(void);
void lcd_update(void);

int main(void)
{
    DelayInit();
    init_lcd();
    init_rotary_encoder();

    while (1)
    {
        lcd_update();
    }
}

void init_lcd()
{
    // Initialize LCD
    lcd16x2_init(LCD16X2_DISPLAY_ON_CURSOR_OFF_BLINK_OFF);
}

void init_rotary_encoder()
{
    GPIO_InitTypeDef GPIO_InitStruct;

    // Step 1: Initialize GPIO as input for rotary encoder
    // PB7 (TIM4_CH2) (encoder pin A), PB6 (TIM4_CH1) (encoder pin B)
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_7 | GPIO_Pin_6;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    // Step 2: Setup TIM4 for encoder input
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
    TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12, 
        TIM_ICPolarity_Rising, TIM_ICPolarity_Falling);
    TIM_Cmd(TIM4, ENABLE);
}

void lcd_update()
{
    // Get encoder value
    enc_cnt = TIM_GetCounter(TIM4);

    // Print encoder value
    sprintf(enc_cnt_buf, "%i", enc_cnt);
    lcd16x2_clrscr();
    lcd16x2_puts(enc_cnt_buf);

    DelayMs(250);
}

8 comments:

  1. Hands-On STM32: Basic Peripherals with HAL
    Is there any way to teach IAP(In Application Programming)?

    ReplyDelete
    Replies
    1. IAP is not available yet.
      In the future I have a plan to make Hands-On STM32: Advanced Peripherals with HAL, I think I will include this.

      Delete
  2. Can I use this code with STM32F072RBT6?
    I have two hall sensor and with that I need to run both motor parallel is it possible?

    ReplyDelete
    Replies
    1. hey i also have the same prob u fixed it

      Delete
  3. Hi Thanks for the explenation and code. Can you tell me how much the vcc in your scematic is?

    ReplyDelete
  4. Hi,
    how to install a STM32F103C8 library in arduino ide for this ?

    #include "stm32f10x.h"
    02
    #include "stm32f10x_rcc.h"
    03
    #include "stm32f10x_gpio.h"
    04
    #include "stm32f10x_tim.h"
    05
    #include "delay.h"
    06
    #include "lcd16x2.h"
    07
    #include
    Alfred

    ReplyDelete
    Replies
    1. in keil you need just set the path to the folder containing *.h and *.c files of the libraries. I think in arduino the same or you need copy them to a specific folder. PS: arduino decrease your power depends to stm32 for same price

      Delete
  5. Thank you. I ported the code for Nucleo-F401RE with HAL Library provided by CubeMX Project. It is working good.

    ReplyDelete