|
|
View previous topic :: View next topic |
Author |
Message |
JAM2014
Joined: 24 Apr 2014 Posts: 138
|
Reading a rotary encoder..... |
Posted: Mon Apr 18, 2016 4:19 pm |
|
|
Hi All,
I'm updating an older design to replace three momentary pushbuttons with a rotary encoder. This is a quadrature type encoder with 'A' and 'B' outputs and Gnd, as well as two other pins for an integral momentary switch. The encoder will be used for data entry and configuration changes only, so isn't particularly high speed.
The original design is based on a 18F45K50 device, and has the original switches connected to RD4, RD5, and RD6. It seems these aren't a good choice for the encoder because I can't use a state change interrupt to detect when something happens. Even if I could, there are a lot of other things connected to port B that I don't want to disturb when reading the port. I've tried reading the encoder with a timed interrupt, but the results are a bit unpredictable. Pins RA0, RA1, and RA2 can be re-purposed for the encoder, and then I can use the RA interrupt to detect movement of the encoder, or a push of the switch. I want to try this on an old board before committing to a new PCB, so I'm looking for some feedback before I start cutting and hacking an old PCB!
Am I headed in the 'right' direction with my thinking??
Thanks,
Jack |
|
|
temtronic
Joined: 01 Jul 2010 Posts: 9228 Location: Greensville,Ontario
|
|
Posted: Mon Apr 18, 2016 6:59 pm |
|
|
Found this in the code library...
http://www.ccsinfo.com/forum/viewtopic.php?t=54940
... it may be of use to you.
I've always used US Digital encoder chips ( and encoders) for the high tech stuff for the past 15-20 years as they need a LOT of counts (4096+/rev) at moderate speed too. For a simple mechanical 'selection' encoder, that code should work for you.
If not, consider a 'dedicated' encoder chip.... ot maybe a pin compatible newer PIC with QENC perihperal already inside.
options, always look for options !
Jay |
|
|
Ttelmah
Joined: 11 Mar 2010 Posts: 19516
|
|
Posted: Tue Apr 19, 2016 1:17 am |
|
|
The key thing to do, is first actually measure the pulse frequency from your encoder. Stick it on a scope, spin it as fast as you think likely, and look at the frequency from one output.
For a standard quadrature encoder, your code _must_ sample no slower than 4* for each cycle (this is the standard 'Nyquist-Shannon' 2* per cycle, but then handling the two separate 90degree cycles involved). This is an absolute minimum, and in fact will need to be exceeded significantly, but then gives you at least an 'idea' of how often the routine needs to be called.
If (for instance), this is 16 pulses/rev, and you can spin it at 500RPM max, then there is a complete cycle for each 'pulse', so the sampling would need to be at faster than 533Hz ((500*16*4/60). Quite easy with (say) a 1KHz tick interrupt.
Now there are lots of things that could be going wrong. If for instance the encoder gives much faster pulses than this, then you will increasingly be likely to have problems missing pulses. Then if this is a mechanical switch, you have the possibility (likelyhood....), of switch bounce. Honestly consider a hardware solution for this (at slower speeds, a software solution can be used, but it is worth 'helping' the code here. Look at the filters shown in this thread (simple):
<http://www.eevblog.com/forum/projects/hardware-debouncing-a-rotary-encoder/>
Particularly the design at the very end of the thread.
Your inputs are Schmitt type, so the addition of just a couple of capacitors and four resistors, should make a huge difference.
Using IOC, might actually make things worse!. If the signals are as nasty as those shown in the thread above, your processor might be called dozens of times per edge, and cause other things to grind to a halt!. Start by trying to get the signals reasonable, before worrying about the code.
I suspect that with some hardware filtering, you may even find your existing code will start to work. I'd be very suspicious that bounce is 90% of your problem..... |
|
|
JAM2014
Joined: 24 Apr 2014 Posts: 138
|
|
Posted: Tue Apr 19, 2016 4:03 pm |
|
|
Hi,
I should have mentioned that I already put a simple RC filter on each output of the encoder, and the output is very, even surprisingly, clean! When I rotate the knob as fast as I can, the total pulse period is 56mS, or about 18 Hz.
So, are you saying that I should be sampling the encoder outputs about every 14 mS? I'm assuming that since I *may* see the same pulse multiple times by 'oversampling' at 4X that I'll have to wait for each output to go high again before sensing another transition?
Away from the bench today, but I'll be in a position to make some tests in the morning!
Jack |
|
|
Ttelmah
Joined: 11 Mar 2010 Posts: 19516
|
|
Posted: Wed Apr 20, 2016 2:03 am |
|
|
Your sense code needs to decode 'changes'. If you look at the code Jay points you to, the very first line of the incremental routine, reads the bits, and then tests if there is a change (if(new==old)). It only updates the position, if there has been a change.
At your low frequency, you could simply put the incremental routine from his code into an interrupt at perhaps 100HZ (overkill, but is quite a nice 'tick' to have, and therefore might be useable for other things as well). Assuming you put the actual encoder bits on D4 & D5 (A=D4, B=D5):
Code: |
//Your header stuff for processor clock etc..
#use fast_io(D)
unsigned int8 old;
signed int16 position=0;
int1 udflag=FALSE;
#INT_TIMERx //for whatever timer you use
void tick(void)
{
int8 new;
int8 value;
new=input_d()&0x30; //Masks D4 & D5
value=new^old;
if (value==0)
return; //no changes
//Code from here on, now functions as if this was an IOC
//'value', now has the bit(s) set, which have changed
if (bit_test(value,4))
{
//Here the low bit has changed
if (bit_test(new,4))
{
//Here a rising edge on A
if (bit_test(new,5)) --position;
else ++position;
}
else
{
//Here a falling edge on A
if (bit_test(new,5)) ++position;
else --position;
}
}
else
{
//Here the high bit (B) must have changed
if (bit_test(new,5))
{
//Here a rising edge on B
if (bit_test(new,4)) ++position;
else --position;
}
else
{
//Here a falling edge on B
if (bit_test(new,4)) --position;
else ++position;
}
}
old=new;
udflag=true;
}
//This is super efficient quadrature handling code for a PIC.
//If you work out the logic, it just does one xor, and then two bit tests to
//update the position
//Then in the main:
void main(void)
{
//1 - setup the interrupt to tick at a sensible rate - up to you
//Now you need to setup the TRIS for port D
set_tris_D(0bxx11xxxx);
//You need to set the other bits as you need (the 'x's).
//D4 & D5 need to be inputs
old=input_d()&0x30; //Masks D4 & D5
//enable the interrupt (for whatever timer you are using).
//Then in your main code:
while (TRUE)
{
if (udflag)
{
udflag=FALSE;
//here 'position' has changed
}
//your other code
}
}
|
Be aware that to do a byte-wide read on the port, and not change the I/O directions on the other bits, 'fast_io' needs to be used. Hence you will need to setup (and control) the TRIS as needed. I've only set the two bits assumed for the actual decoder.
This code sets a flag (udflag), whenever the position changes.
The quadrature code here is designed to be as efficient as possible on a PIC. There are four paths through the code depending on which bit(s) have changed. |
|
|
JAM2014
Joined: 24 Apr 2014 Posts: 138
|
|
Posted: Wed Apr 20, 2016 3:56 pm |
|
|
Hi,
Well, it kinda, sorta works! The problem is that for every step to the next detent, I'm incrementing 'position' too much, like by a factor of 4. BTW, the encoder I'm using is a CTS 290 series with 20 detents per revolution. I found this little nugget on the web which might explain what I'm seeing:
Quote: |
The odd thing about the CTS encoder is that for each rotary detent, it outputs all 4 codes instead of one code as with the Grayhill. Once you figure this out and change the program accordingly, the encoder works just fine.
|
I haven't figured out exactly how to 'change the program accordingly', but at least at this point, it doesn't seem to be a timing issue related to how often the encoder is being sampled!
Jack |
|
|
necati
Joined: 12 Sep 2003 Posts: 37 Location: istanbul
|
|
|
Ttelmah
Joined: 11 Mar 2010 Posts: 19516
|
|
Posted: Thu Apr 21, 2016 1:10 am |
|
|
It would do.
A single detent, gives you four edges. The rising and falling edge on channel A, and the rising an falling edge on channel B.
A 16 line encoder, gives 64 positions.
The code is written to decode all four edges.
Just divide by four if you only want the low accuracy.
It is possible to write the code to only read single edges, but actually saves nothing.
That is nothing 'odd' about the CTS encoder, that is how quadrature encoders work (they output two signals in 'quadrature' - 90 degree phase shifted, for each detent). |
|
|
JAM2014
Joined: 24 Apr 2014 Posts: 138
|
|
Posted: Wed May 18, 2016 10:37 am |
|
|
Hi All,
I'm just getting back to this project after a few weeks of travel. I've taken the code posted by Ttelmah, and modified it with a global 'EncoderTick' variable that is used to count off the 4 transitions of each input for every detent on the encoder. Again, my rotary encoder provides 4 edges for each detent, and I want to increment a variable by +/- 1 for each detent click!
Code: |
#include <18F45K50.h>
#fuses HSH,PLL3X,NOWDT,NOPROTECT,NOLVP,NODEBUG,NOFCMEN,CPUDIV3
#use delay(crystal=16MHz, clock=16MHz, USB_FULL)
//-----< General Program Defines >-----
#define Pwr_LED Pin_B0 // Power LED
#define Rotary_A Pin_D4 // Rotary Encoder 'A' terminal
#define Rotary_B Pin_D5 // Rotary Encoder 'B' terminal
#define Rotary_SW Pin_D6 // Rotary Encoder Switch
#define Serial_TxD Pin_C6 // Serial Tx data
#define Serial_RxD Pin_C7 // Serial Rx Data
#use rs232(baud=9600, xmit=Serial_TxD, rcv=Serial_RxD, Errors, stream = Console)
//Your header stuff for processor clock etc..
#use fast_io(D)
unsigned int8 old;
signed int16 position=0;
int1 udflag=FALSE;
signed EncoderTick = 0;
#INT_TIMER0 //for whatever timer you use
void tick(void)
{
int8 new;
int8 value;
new=input_d()&0x30; //Masks D4 & D5
value=new^old; //exclusive OR logical operator...
if (value==0)
return; //no changes
//Code from here on, now functions as if this was an IOC
//'value', now has the bit(s) set, which have changed
if (bit_test(value,4))
{
//Here the low bit has changed
if (bit_test(new,4))
{
//Here a rising edge on A
if (bit_test(new,5)) --EncoderTick;
else ++EncoderTick;
}
else
{
//Here a falling edge on A
if (bit_test(new,5)) ++EncoderTick;
else --EncoderTick;
}
}
else
{
//Here the high bit (B) must have changed
if (bit_test(new,5))
{
//Here a rising edge on B
if (bit_test(new,4)) ++EncoderTick;
else --EncoderTick;
}
else
{
//Here a falling edge on B
if (bit_test(new,4)) --EncoderTick;
else ++EncoderTick;
}
}
if (EncoderTick == 4)
{
position++;
EncoderTick=0;
udflag=true;
}
if (EncoderTick == -4)
{
position--;
EncoderTick=0;
udflag=true;
}
old=new;
}
//This is super efficient quadrature handling code for a PIC.
//If you work out the logic, it just does one xor, and then two bit tests to
//update the position
//Then in the main:
void main(void)
{
//Now you need to setup the TRIS for port D
set_tris_D(0b00110000);
//You need to set the other bits as you need (the 'x's).
//D4 & D5 need to be inputs
old=input_d()&0x30; //Masks D4 & D5
//enable the interrupt (for whatever timer you are using).
//Here we setup Timer 0, and enable Global interrupts
set_timer0(0);
setup_counters( RTCC_INTERNAL, RTCC_DIV_256 | RTCC_8_BIT); //16mS interrupt - 1/(Fosc/4/256/256)
enable_interrupts(INT_RTCC);
enable_interrupts(GLOBAL);
//Then in your main code:
while (TRUE)
{
if (udflag)
{
udflag=FALSE;
//here 'position' has changed
fprintf(Console, "Position: %ld\n\r", position);
}
//your other code
}
}
|
This modified code seems to work reasonably well, but occasionally seems to miss an update when the encoder is rotated. It's not really provide a 100% positive 'feel' to the user, so I'm looking for suggestions to improve the modifications I've made!
Ideas?
Jack
Last edited by JAM2014 on Wed May 18, 2016 11:33 am; edited 1 time in total |
|
|
PCM programmer
Joined: 06 Sep 2003 Posts: 21708
|
|
Posted: Wed May 18, 2016 10:46 am |
|
|
Quote: |
but occasionally seems to miss an update when the encoder is rotated.
signed int16 position=0;
#INT_TIMER0
void tick(void)
{
.
.
.
position++;
.
.
.
}
|
Post your code in main() or elsewhere, where you use the 'position' variable. |
|
|
JAM2014
Joined: 24 Apr 2014 Posts: 138
|
|
Posted: Wed May 18, 2016 11:34 am |
|
|
Hi,
I've just updated my last post to include the full test program!
Thanks,
Jack |
|
|
Ttelmah
Joined: 11 Mar 2010 Posts: 19516
|
|
Posted: Wed May 18, 2016 1:07 pm |
|
|
You are almost certainly not sampling fast enough.
The timer needs to occur at least _once_ in every quadrant. Your pulse period is 56mSec, so you need something faster than every 14mSec (more to be comfortable). You are using 16mSec. Use /128, not /256. This is then fast enough.
Also, you really should copy 'position' to a local variable, before trying to print it. Printing takes a long time. If position changes during the print, you will get an invalid value. So:
Code: |
int16 local_position;
//Then in the code where udflag is set:
do {
local_position=position;
} while (local_position != position);
//then print 'local_position'
|
|
|
|
JAM2014
Joined: 24 Apr 2014 Posts: 138
|
|
Posted: Wed May 18, 2016 2:40 pm |
|
|
Hi,
Doh! (accompanied by headslap!)
Those changes made all the difference in the world, especially bumping up the 'sample rate'! Many thanks!
Jack |
|
|
Ttelmah
Joined: 11 Mar 2010 Posts: 19516
|
|
Posted: Thu May 19, 2016 1:55 am |
|
|
Yes. The need to copy, shouldn't actually occur, unless the timer ticks during the print, but 'better safe'.
Good. |
|
|
diode_blade
Joined: 18 Aug 2014 Posts: 55 Location: Sheffield, South Yorkshire
|
|
Posted: Thu May 19, 2016 4:54 am |
|
|
Just wondering why you don't just use an 18f4431 chip with a quadrature encoder interface, you can set it up in velocity mode or set it up as a positional counter and the hardware does all the work, just read the required registers.
As taken from 18f4431 data sheet
The Quadrature Encoder Interface (QEI) decodes
speed and motion sensor information. It can be used in
any application that uses a quadrature encoder for
feedback. The interface implements these features:
• Three QEI inputs: two phase signals (QEA and
QEB) and one index signal (INDX)
• Direction of movement detection with a direction
change interrupt (IC3DRIF)
• 16-bit up/down position counter
• Standard and High-Precision Position Tracking
modes
• Two Position Update modes (x2 and x4)
• Velocity measurement with a programmable
postscaler for high-speed velocity measurement
• Position counter interrupt (IC2QEIF in the PIR3
register)
• Velocity control interrupt (IC1IF in the PIR3
register)
Just a thought...
Dave |
|
|
|
|
You cannot post new topics in this forum You cannot reply to topics in this forum You cannot edit your posts in this forum You cannot delete your posts in this forum You cannot vote in polls in this forum
|
Powered by phpBB © 2001, 2005 phpBB Group
|