Interface a Rotary Encoder the right way.


So whilst reading my favorite odyssey found here  I stumbled onto something I never spotted before. It turns out that the general purpose timers support hardware interfacing with an incremental encoder.  This means no more interrupts and no need to increment whatever variable you had. Now all you have to do is get the value from the Count register in the timer. Did I also mention it takes care of figuring out which direction you are turning it? Amazing! Lets talk about it.

Encoder Interface Mode

Verbatim from the reference manual of the STM32F103 page 392 or Section 15.3.12 
"Encoder interface mode acts simply as an external clock with direction selection. This 
means that the counter just counts continuously between 0 and the auto-reload value in the 

TIMx_ARR register (0 to ARR or ARR down to 0 depending on the direction). So the user 

must configure TIMx_ARR before starting. In the same way, the capture, compare, 

prescaler, trigger output features continue to work as normal."

What this means is that the Count (CNT) in the timer is incremented or decremented depending on which direction you turn the encoder. 

Furthermore on page 393 the reference manual gives us some sample settings we can use, which are as follows:

  1. CC1S= ‘01’ (TIMx_CCMR1 register, TI1FP1 mapped on TI1)
  2. CC2S= ‘01’ (TIMx_CCMR2 register, TI2FP2 mapped on TI2)
  3. CC1P= ‘0’, CC1NP = ‘0’, IC1F =’0000’ (TIMx_CCER register, TI1FP1 noninverted,    TI1FP1 = TI1)
  4. CC2P= ‘0’, CC2NP = ‘0’, IC2F =’0000’ (TIMx_CCER register, TI2FP2 noninverted, TI2FP2=TI2)
  5. SMS= ‘011’ (TIMx_SMCR register, both inputs are active on both rising and falling edges)
  6. CEN = 1 (TIMx_CR1 register, Counter is enabled)

Steps 1 - 2 maps channel 1 and 2 as inputs to Timer inputs 1 and 2. 
Steps 3 - 4 set up the channels to trigger on rising edge (non-inverted)
Step 5 you can also select if it increments based on one of the signal  pulses relation to the other, or both edges of both pulses, this is done via the SMS bits in the SMCR register. I have highlighted the relevant options for those bits below:
Step 6 is to enable the timer. 

You also still have to setup the timer Auto reload register. This will tell the timer the max number it can count up to. Being a 16 bit register this gives you a max number of 0xFFFF (65535). After that you just enable this timer and read the count from the CNT register and that is all. Check out the video below and read through the code. In the code I have implemented both a regular interrupt driven version versus the Timer Hardware version. Obviously the Timer hardware version is much more efficient and faster because it frees up the CPU from doing calculations, jumping in and out of thread and handler mode, jumping to a handler routine etc....

Here are my connections:



  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
/* Includes */

#include "stm32f10x.h"
#include "printMSG.h"
#include "stdint.h"

//-------| prototypes |------------
void EXTI1_IRQHandler(void);
void delayMS(uint16_t ms);
uint32_t val = 0;
uint32_t MSTICKS = 0;
void init_debug(void);

//encoder functions
void init_interrupt_version(void);
void init_hardware_timer_version(void);

int main(void)
{

 init_debug();

 //init_interrupt_version();
 init_hardware_timer_version();

 while(1)
 {
  printMsg("d: %d\n",TIM4->CNT); // for Timer hardware version
  //printMsg("d: %d\n",val); // for interrupt version
  delayMS(50);
 }

}
void init_hardware_timer_version(void)
{
 RCC->APB2ENR |= RCC_APB2ENR_AFIOEN | RCC_APB2ENR_IOPBEN;
 RCC->APB1ENR |= RCC_APB1ENR_TIM4EN; //AFIO might not even be needed?

 //GPIO must be input floating which is default so no code to write for that

 // value to count up to : 16 bit so max is 0xFFFF = 65535
 TIM4->ARR = 0xFFFF;

 //per datasheet instructions
 TIM4->CCMR1 |= (TIM_CCMR1_CC1S_0 | TIM_CCMR1_CC2S_0 );  //step 1 and 2
 TIM4->CCER &= ~(TIM_CCER_CC1P | TIM_CCER_CC2P);  // step 3 and 4
 TIM4->SMCR |= TIM_SMCR_SMS_0 | TIM_SMCR_SMS_1;   //step 5
 TIM4->CR1 |= TIM_CR1_CEN ;     //step 6
}
void init_interrupt_version(void)
{
 RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
 RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
 EXTI->IMR |= 1<<1; //enable interrupt on EXTI-1
 EXTI->IMR |= 1; //enable interrupt on EXTI-1
 NVIC_EnableIRQ(EXTI1_IRQn); //enable the IRQ line that corresponds to EXT-1
 NVIC_EnableIRQ(EXTI0_IRQn); //enable the IRQ line that corresponds to EXT-1
 EXTI->RTSR |= 1<<1; //enable rising edge interrupt on line 1
 EXTI->FTSR |= 1; //enable rising edge interrupt on line 1
 AFIO->EXTICR[0] |= AFIO_EXTICR1_EXTI1_PB; // set interrupt to be on port B
 AFIO->EXTICR[0] |= AFIO_EXTICR1_EXTI0_PB; // set interrupt to be on port B
}

void EXTI1_IRQHandler(void)
{
 EXTI->PR |= 1<<1; //clear pending request interrupt flag
 if(GPIOB->IDR & 0x01) //high  on PB 0
 {
  val++;
 }
 else
 {
  val--;
 }
}
void EXTI0_IRQHandler(void)
{
 EXTI->PR |= 1<<1; //clear pending request interrupt flag
 if(GPIOB->IDR & 0x02) //if high  on PB 1
 {
  val++;
 }
 else
 {
  val--;
 }
}
void init_debug(void)
{
 SysTick_Config(SystemCoreClock /1000);

 printMsg_config_Type printer;
 printer.TX_pinNumber = 9;
 printer.Uart_instance = USART1;
 printer.tx_port = GPIOA;
 printMsg_init(printer);
 printMsg("Debug UART initialized | SystemCore Clock : %d", SystemCoreClock);
}

void SysTick_Handler(void)
{
 MSTICKS++;
}

void delayMS(uint16_t ms)
{
 MSTICKS = 0;

 while(MSTICKS < ms);
}

Comments

  1. Eddie, this is excellent stuff. Currently I am not able to work on MCUs, but I will keep this page bookmarked. Thanks. BTW, what you think about colab with me?

    ReplyDelete
  2. Well, commented from my other account (wild...), but it is "unknown" by blog post. Are you willing to make collaboration with me (Milan Karakas on YouTube). Just give me some contact address, some email or something. Even phone number will be fine. Thank you in advance for your consideration.

    ReplyDelete
  3. This is some great stuff young grasshopper!!!

    ReplyDelete
  4. #include "printMSG.h"
    #include "stdint.h"
    where is these libraries? and thank you so much

    ReplyDelete
  5. I just watched your youtube videos on DMA and timers. Thanks for taking time to explain properly and actually referencing the manual, so many "tutorials" that are just step-by-step instructions how to do something in an IDE. I'm happy to have found your videos!

    Just got the rotary encoder going nicely in rust following your example :)

    ReplyDelete
  6. Thank you for sharing this....

    ReplyDelete
  7. This comment has been removed by the author.

    ReplyDelete
  8. great work if i have Vcc 5V for encoder so can i connect encoder with stm32f103 is it safe for input signal to stm or that can damage the pin.

    ReplyDelete
  9. Hello
    nice work , thank you
    Do have solution for 32bit hardware counter
    or 16 bit counter with interrupt in order to keep
    32 bit position ?
    Thank you

    ReplyDelete
  10. Many thanks for your contribution. It was enough to revive an old project of mine.

    ReplyDelete
  11. In case if anyone wanted to know if timer is getting overflow or underflow then interrupt can be used. This can be initialised as
    TIM4->DIER|=TIM_DIER_UIE;
    NVIC_EnableIRQ(TIM4_IRQn);
    If anyone need to check the direction of counting, it can be checked with the TIM_CR1_DIR bit in CR1 register.

    ReplyDelete
Share your comments with me

Archive

Contact Form

Send