PrinceNai
Joined: 31 Oct 2016 Posts: 530 Location: Montenegro
|
Another rotary encoder reader (KY-040) |
Posted: Thu May 01, 2025 3:45 pm |
|
|
Below is the interrupt driven code to read and debounce a KY-040 type rotary encoder. Any timer can be used and any input with edge detection capability. My encoder is connected to PORTE, so CCP3 interrupt was used. After some testing it seems the code works fine. It does one thing, though. If you rotate the shaft for half a step and then go back, it detects a step. That happens because only one line is checked for transitions, so it doesn't know the step wasn't completed. As it is the case with these encoders, it doesn't matter which line goes where, simply adjust the direction in software as needed or switch the wires.
Code: |
////////////////////////////////////////////////////////////////////////////////
// KY-040 encoder has two outputs, seen as CLK and DATA, phase shifted. //
// Most implementations keep track of prevous states to figure out //
// the direction. But if you look those two signals as clock and data, //
// there is another very simple method: //
// observe the FALLING (or rising, doesn't matter) edge on the CLK line. //
// DATA line is always stable in that moment, no debouncing is needed there. //
// In case of a falling edge, direction can be determined like this: //
// //
// -if the DATA line is HIGH at the time of the falling edge, one direction //
// -if the DATA line is LOW at the time of the falling edge, other direction //
// //
// The problem here is the noise on the contacts or bouncing. This //
// approach uses two interrupts, one with edge detection capability and timer //
// interrupt to wait for the bouncing to stop. Since it is all interrupt //
// driven, waiting doesn't mean main is stopped during debouncing. //
// //
// It works good enough for every day applications. The only issue I noticed //
// so far are cases when the encoder isn't turned for a full indent, but only // //
// begins the movement and than falls back. Since transitions on data line are//
// not tested, a valid count is detected, even if it didn't physically happen.//
////////////////////////////////////////////////////////////////////////////////
#include <18F46K22.h>
#device ADC = 10
#FUSES NOWDT, PUT // No Watch Dog Timer, NOPUT for debug
#FUSES CCP3E0 // CCP3 input routed to PIN_E0, since it is multiplexed with PIN_B5
//#device ICD = TRUE
#use delay(internal = 32000000)
#use rs232(baud = 115200, parity = N, UART2, bits = 8, stream = DEBUG, errors)
#use i2c(Master, Slow, sda=PIN_C4, scl=PIN_C3, force_hw)
#include <string.h>
// ---------------------------------------------------------------------------
// encoder connections
#define ENCODER_CLK PIN_E0
#define ENCODER_DATA PIN_E1
#define ENCODER_BUTTON PIN_E2
#define MARKER PIN_B5 // visual output
#define CW 1 // direction of movement. Completely arbitrary.
#define CCW 0
#define DEBOUNCE_TIME 33535 // timer preload for the desired debounce duration. 4ms at 32MHz in this case
//#define DEBOUNCE_TIME 0 // there is no science behind this delay. Use whatever works, as long as it covers the bounce duration.
int8 EncoderDirection; // holds the direction of the last encoder movement
int8 EncoderMoved = FALSE; // flag to indicate encoder movement
signed int16 EncoderCount = 0; // counter for encoder steps
int8 active_state_ENCODER_BUTTON; // ENCODER_BUTTON debounce variables
int8 last_state_ENCODER_BUTTON;
int8 ButtonPressed = FALSE; // ENCODER_BUTTON flag
int16 ButtonCount = 0; // ENCODER_BUTTON presses counter
int8 GetEncoderData; // flag to indicate encoder data must be sampled. Could be done also with bit testing of bit0 of CCPCON3
// which tells us what the edge of the transition was (how the interrupt was set up, falling or rising edge)
// ***************************** INTERRUPTS **********************************
// ***************************************************************************
#INT_CCP3
void CCP3_isr(void)
// CCP interrupt is used because it has edge detect capability. Could be any
// other interrupt that can do that.
{
set_timer0(DEBOUNCE_TIME); // set timer to overflow in 4ms
// Signal on CLK line of the encoder changed state from HIGH to LOW, indicating
// that encoder moved and that data is valid here. Test the other (DATA) line
// when this transition is detected in order to find out the direction
// of the movement. Once done, encoder contact debouncing is performed using timer
// interrupt.
// CW and CCW directions are arbitrary, look at them as one way - other way.
// Change according to preferences.
if(GetEncoderData == TRUE){ // take data only on the falling edge of the clock
GetEncoderData = FALSE;
// REMOVE
output_low(MARKER);
delay_cycles(8); // provide a nice visual indication for
output_high(MARKER); // a logic analyser or oscilloscope to see if interrupts are firing as we want them to
// if counting goes the wrong way for your application, comment out the first one
// and uncomment the second one. Adjust CW and CCW as desired.
if(input_state(ENCODER_DATA) == 1){ // check the state of "data" line
// if(input_state(ENCODER_DATA) == 0){
EncoderCount++; // increment encoder count
EncoderDirection = CW; // not really needed, but indicate the direction too
}
else{
EncoderCount--; // decrement encoder count
EncoderDirection = CCW; // get the direction
}
EncoderMoved = TRUE; // indicate main that we had movement
}
// Everything done in the code above might work just fine in the ideal world
// with nice, clean transitions. But we are not there, meaning contacts always
// "bounce", causing multiple transitions every time, potentialy causing multiple
// interrupts. The solution implemented here is to disable the interrupt and wait
// till the bouncing stops before re-enabling it again, both on falling and the
// rising edge of the CLK.
disable_interrupts(INT_CCP3); // disable edge detection interrupt for 4ms, to effectively wait out the bouncing
// and avoid repeated interrupts for the same transition
}
// ---------------------------------------------------------------------------
#INT_TIMER0
void TIMER0_isr(void)
{
// 4ms later we are here. The bouncing, if we are lucky, stopped. We are using
// timer to wait out the noise encoder makes on its contacts when turning. We are
// debouncing both transitions of the CLK signal, high to low and low to high. That
// is the reason for alternating the active edge of the CCP3 interrupt. First the
// clock changes from high to low. We sample data and debounce that transition.
// Then we change the active edge of CCP interrupt to low to high, which happens
// when encoder "clicks" one step. Here rising edge is debounced and we prepare
// for debouncing the falling edge.
// CCP interrupt changes the values of the timer, so it can't be used for any
// kind of time measurements. Use any other 16bit timer if that is a problem.
// set the direction of the next interrupt according to the debounced, stable state of CLK
// CLK is stable 1
if(input_state(ENCODER_CLK) == 1){
setup_ccp3(CCP_CAPTURE_FE); // set next CCP interrupt on the falling edge of the CLK line
GetEncoderData = TRUE; // signal that data must be sampled then
}
// CLK is stable 0
else{
setup_ccp3(CCP_CAPTURE_RE); // next interrupt will happen on a rising edge of CLK line
// where we will only wait for the contact noise to pass,
} // and re-enable the interrupt on falling edge
clear_interrupt(int_CCP3); // clear the CCP interrupt flag
enable_interrupts(INT_CCP3); // and wait for a transition on CLK line
// ENCODER_BUTTON DEBOUNCING..........................................................
active_state_ENCODER_BUTTON = input_state(ENCODER_BUTTON); // read ENCODER_BUTTON
if((last_state_ENCODER_BUTTON == 1) && (active_state_ENCODER_BUTTON == 0)){
ButtonPressed = TRUE; // raise "ENCODER_BUTTON PRESSED" flag. Must be cleared in software.
ButtonCount++;
}
last_state_ENCODER_BUTTON = active_state_ENCODER_BUTTON; // prepare values for next time
}
// ************************* END OF INTERRUPTS *******************************
// ***************************************************************************
// ****************************** MAIN ***************************************
void main()
{
output_float(ENCODER_CLK); // ENCODER_CLK is input
output_float(ENCODER_DATA); // ENCODER_DATA is input
output_float(ENCODER_BUTTON); // ENCODER_BUTTON is input
output_high(MARKER); // turn off MARKER
setup_timer_0(T0_INTERNAL|T0_DIV_1); // 8,1 ms overflow at 32MHz
setup_timer_1(T1_INTERNAL|T1_DIV_BY_1); // TMR1 is used for CCP3
setup_ccp3(CCP_CAPTURE_FE); // set first CCP3 interrupt on a falling edge
enable_interrupts(INT_CCP3); // enable interrupts
enable_interrupts(INT_TIMER0);
enable_interrupts(GLOBAL);
// ........................... END OF SETUP ..................................
while(TRUE)
{
if(EncoderMoved == TRUE){
EncoderMoved = FALSE;
fprintf(DEBUG, "%ld\n\r", EncoderCount); // print EncoderCount values to debug port
}
if(ButtonPressed == TRUE){
ButtonPressed = FALSE;
EncoderCount = 0;
fprintf(DEBUG, "Encoder switch pressed %lu times. Encoder count reset.\n\r", ButtonCount); // print ButtonCount values to debug port
}
} // end while(TRUE)
} // end main
|
|
|