Skip to main content

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

Post a Comment

Popular posts from this blog

STM32L0/F0/F3 I2C : Part 3 RX TX via DMA

Most modern microcontroller come with a peripheral called DMA which allows for an even more hands-off approach. The Direct Memory Access controller will get a tutorial of it's own in the future. However, it is so simple to use that I can easily explain the required bits in this tutorial without feeling like I will overwhelm the reader. In this iteration of the I2C series I will cover how to TX and RX date on the peripheral in conjunction with the DMA controller. 

First let me briefly explain in a high level what the DMA controller does. It allows you to transfer data in 3 ways: Peripheral to memoryMemory to peripheralMemory to memory. What does this mean? For example, when you receive data on I2C in your RXDR register, you can have the DMA controller move that data received into an array. At the end of the day your variables are just memory locations, so if you have declared an array it has an address associated with it, this is address is what the DMA considers  "Memory"…

STM32 I2C Does it suck?

Recently I have been annoyed with  bugs in the I2C implementation in ST's F1/L1/F40/F2 series, and  any microcontroller made by ST before 2012. There appears to be, what I can only describe as, a race condition when attempting to receive data in I2C interrupt(low priority)  or polling mode.





Just to be clear a race condition is defined as , verbatim from wiki: "the behavior of an electronics, software, or other system where the system's substantive behavior is dependent on the sequence or timing of other uncontrollable events. It becomes a bug when one or more of the possible behaviors is undesirable."
I was humming along trying to make my YouTube tutorials when I arrived to the I2C protocol implementation. I have used I2C with the F1 series plenty of times, however I have never had the need to receive data from a slave. However for the sake of completeness I decided to implement an RXing routine in I2C for my tutorial. Only to find statements like this in the referen…