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:
Steps 1 - 2 maps channel 1 and 2 as inputs to Timer inputs 1 and 2.
Furthermore on page 393 the reference manual gives us some sample settings we can use, which are as follows:
- CC1S= ‘01’ (TIMx_CCMR1 register, TI1FP1 mapped on TI1)
- CC2S= ‘01’ (TIMx_CCMR2 register, TI2FP2 mapped on TI2)
- CC1P= ‘0’, CC1NP = ‘0’, IC1F =’0000’ (TIMx_CCER register, TI1FP1 noninverted, TI1FP1 = TI1)
- CC2P= ‘0’, CC2NP = ‘0’, IC2F =’0000’ (TIMx_CCER register, TI2FP2 noninverted, TI2FP2=TI2)
- SMS= ‘011’ (TIMx_SMCR register, both inputs are active on both rising and falling edges)
- 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:
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); } |
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?
ReplyDeleteWell, 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.
ReplyDeleteThis is some great stuff young grasshopper!!!
ReplyDeleteque lo que tigre
Delete#include "printMSG.h"
ReplyDelete#include "stdint.h"
where is these libraries? and thank you so much
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!
ReplyDeleteJust got the rotary encoder going nicely in rust following your example :)
Thank you for sharing this....
ReplyDeleteThis comment has been removed by the author.
ReplyDeletegreat 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.
ReplyDeleteHello
ReplyDeletenice 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
Many thanks for your contribution. It was enough to revive an old project of mine.
ReplyDeleteIn case if anyone wanted to know if timer is getting overflow or underflow then interrupt can be used. This can be initialised as
ReplyDeleteTIM4->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.