News:

Let's find out together what makes a PIC Tick!

Main Menu

Quadratic encoder problem

Started by Parmin, Apr 12, 2021, 03:20 AM

Previous topic - Next topic

Parmin

G'day

asking for a new problem, this time is for a peripheral device.
In particular, I use a YUMO ISM8060 manual pulse wheel.
http://machinetoolproducts.com/content/Yumo/Yumo%20ISM8060%20Series%20Spec%20Sheet.jpg
This is a quite high-quality encoder that gives a reliable output signal.

Yet somehow I keep losing step counts when I spin the wheel back and forth a few times.

In my code, I use INT interrupt pin to detect every rise of Signal A and read the state of Signal B when this occurs to determine the direction of the wheel spin.  I also use SWord for the location log to be able to record both positive and negative movements.

My PIC is set for 8MHz
and assuming the maximum spin rate of the encoder wheel is 10 rev per second, the minimum time between pulse would be 1ms
Which is well within the capability of the PIC to pick up.

The code snippet is as below.

   Dim LocationCount as SWord

   Symbol InputA = PortB.0
   Symbol InputB = PortB.1

ISR:
  Context Save
    if InputB = 1 then
     LocationCount = LocationCount + 1    ; this is to increment the location count
    else
     LocationCount = LocationCount - 1    ; this is to decrement the location count
    endif
    while InputA = 1                      ; this is to ensure that the pulse input wave is fully done before moving forward
    wend
    INTF = 0
  Context Restore


I have a feeling that the missing steps are due to the wheel changing directions in between the step state of the encoder.

Can anyone help to solve this problem?

 

Stephen Moss

#1
Quote from: Parmin on Apr 12, 2021, 03:20 AMIn my code, I use INT interrupt pin to detect every rise of Signal A and read the state of Signal B when this occurs to determine the direction of the wheel spin.  I also use SWord for the location log to be able to record both positive and negative movements.

I think that your Interrupt on a rise of A is part of your problem, I had a quick look and it appears to be just a 2 bit grey code so....
C  A B
1  0 0
2  1 0 - A rises here so you check B
3  1 1 - A is already high so even if you have exited the ISR no there is no new rise of A, consequently no new interrupt is generated and you miss the change in B.
4  0 1
(Cycle continues looping back to the top 00)

Additionally...
a) If at condition 3 (11) and you go backward (10) you could miss 3 states you will not see a new rise of A until you get back to 3, I think you really need an interrupt on change on both A & B and in both direction (high to low & low to high) if you are not to miss any changes.
b) Inside your ISR the While InputA = 1 loop also means that if you have moved from condition 1 (00) to condition 2 (10) you will be locked in that loop and thus in your ISR until you go back to condition 0 or forward to condition 4 where A goes to 0, resulting in you not detecting a change from condition 2 to condition 3 as it occurs when you are stuck in the While loop.

Parmin

@Stephen Moss

Thank you for the reply.

The issue I have is not likely as what you outlined.

In my program, each pulse of InputA is a clock and inputB which may lead or lag InputA as the direction marker at "precisely" the time where inputA is rising.
I believe this would be the easiest implementation of quadrature decoding, but at the cost of higher granularity of the output.


The method that you outlined would be much more complex since it involves splitting the actual pulse into 4 distinct clock.
To implement this method you would need to precisely measure the time between the pulse inputs and read the states in the mid points of the pulse time, and this would make a very complex programming indeed.

Gamboa

Parmin,

I think that inside the ISR you have to delete the interrupt flag "INT0IF: INT0 External Interrupt Flag bit"
INT0IF = 0

Regards,
Gamboa
Long live for you

Giuseppe MPO

Working with quadrature encoder even with PIC with high clock (64MHz) i managed to minimize the problem but not to remove it completely,
when there is the rotation inversion there may be very fast impulses that the software fails to manage,
this can also happen when the rotation is stopped but there are vibrations on the axis.
Excellent results can be obtained using the PIC18F2431 which has an internal CIP to manage quadrature signals.

Pepe

Device = 18F2550
Declare Xtal = 48

Symbol GIE = INTCON.7  ' Global Interrupt Enable

Symbol TMR0IE INTCON.5  ' TMR0 Overflow Interrupt Enable
Symbol TMR0IF INTCON.2  ' TMR0 Overflow Interrupt

Symbol InputA = PortB.0
Symbol InputB = PortB.1

Symbol PERIOD      100.0      ; 100 us
Symbol Xtl          48000000    ; crystal frequency - 20MHz
Symbol ICLK        (Xtl/4)    ; crystal is divided by four
Symbol SCALE        8          ; prescale by 8 - check for overflow!

Symbol PRELOAD     261-(PERIOD*ICLK/SCALE)/1000000

Dim currentCLK as bit
Dim previousCLK as bit
Dim LocationCount as SWord

'Main program

TMR0L = PRELOAD
On_Hardware_Interrupt GoTo ISR
T0CON = 0xC2
TMR0IE = 1

GIE = 1

Do
.
.
.
.
Loop


ISR:
  Context Save

If TMR0IF = 1 Then                 
                  TMR0L = PRELOAD

currentCLK = InputA

If currentCLK <> previousCLK Then 
    If InputB <> currentCLK Then      
                       Dec LocationCount   
                               Else
                       Inc LocationCount
    End If     
    previousCLK = currentCLK
End If

TMR0IF = 0
End If
Context Restore

Mapo

Hi Parmir,
I have write this simple but efficient ISR routine with initialize Interrupt for RB4 and RB5

Inter_Routine:
                Context Save
                Clear INTCON.0
                INTCON.0 = 1
                                 
                Enc = (PORTB & %00110000)               
                Enc = Enc >> 4
                If Old_Enc = 2 And Enc = 3 Then Inc Pos_Actual
                If Old_Enc = 3 And Enc = 2 Then Dec Pos_Actual
                Old_Enc = Enc

                INTCON.0 = 0
               
                Context Restore


top204

#7
I have only had to use a rotary encoder reader in an interrupt on a few occasions, because the programs that I write for them have been created in the manner of a state machine, where there is no blocking of its flow. Sounds complex but is actually simple to do.

Below is a program I have used on several occasions to read a rotary encoder, but I have adapted it for the latest compiler versions using procedures. It is rather optimised in that the validation table is actually the bits within a 16-bit variable, instead of taking up space as a flash memory table. :-)

'
'   /\\\\\\\\\
'  /\\\///////\\\
'  \/\\\     \/\\\                                                 /\\\          /\\\
'   \/\\\\\\\\\\\/        /\\\\\     /\\\\\\\\\\     /\\\\\\\\   /\\\\\\\\\\\  /\\\\\\\\\\\  /\\\\\\\\\
'    \/\\\//////\\\      /\\\///\\\  \/\\\//////    /\\\/////\\\ \////\\\////  \////\\\////  \////////\\\
'     \/\\\    \//\\\    /\\\  \//\\\ \/\\\\\\\\\\  /\\\\\\\\\\\     \/\\\         \/\\\        /\\\\\\\\\\
'      \/\\\     \//\\\  \//\\\  /\\\  \////////\\\ \//\\///////      \/\\\ /\\     \/\\\ /\\   /\\\/////\\\
'       \/\\\      \//\\\  \///\\\\\/    /\\\\\\\\\\  \//\\\\\\\\\\    \//\\\\\      \//\\\\\   \//\\\\\\\\/\\
'        \///        \///     \/////     \//////////    \//////////      \/////        \/////     \////////\//
'                                  Let's find out together what makes a PIC Tick!
'
' Rotary encoder reader using a bit-table decode mechanism
'
' Written by Les Johnson for the Positron8 BASIC compiler
'
    Device = 18F25K22                           ' Choose the device to compiler for
    Declare Xtal = 16                           ' Tell the compiler, the device will be running at 16MHz
'
' Setup USART1
'
    Declare Hserial_Baud = 9600
    Declare HRSOut_Pin = PORTC.6

    Symbol Enc_Pin1 = PORTB.0                   ' Connects to a pin on the rotary encoder
    Symbol Enc_Pin2 = PORTB.1                   ' Connects to the other pin on the rotary encoder
'
' Create some global variables for the rotary encoder read procedure
'
    Dim Enc_bValidValue As Byte                 ' Holds the previous valid reading from the rotary encoder
    Dim Enc_bStore As Byte                      ' Stores the previous manipulated value from the rotary encoder
    Dim Enc_wTable As Word = %0110100110010110  ' Holds the bit table for valid moves of the rotary encoder
'
' Create some variables for the demo
'
    Dim wCounter As SWord = 0
    Dim bEncValue As SByte

'---------------------------------------------------
' The main program starts here
' Read a rotary encoder and increment or decrement a variable depending on the direction of the rotation
' The value and the direction are transmitted to a serial terminal
'
Main:
    Enc_Setup()                                 ' Setup the program and the pins for the rotary encoder reading

    Do                                          ' Create a loop
        bEncValue = Enc_Read()                  ' Read the rotary encoder
        If bEncValue <> 0 Then                  ' Is the value read from it 0?
            wCounter = wCounter + bEncValue     ' No. So add the value to wCounter
            If bEncValue < 0 Then               ' Was the value read -1?
                HRSOut "CCW: "                  ' Yes. So display that is was counter-clockwise
            Else                                ' Otherwise... The value read was 1. So...
                HRSOut "CW:  "                  ' Display that is was clockwise
            EndIf
            HRSOutLn SDec wCounter              ' Display the count value on the serial terminal
        EndIf
    Loop                                        ' Do it forever

'---------------------------------------------------
' Read a rotary encoder
' Input     : Enc_Pin1 and Enc_Pin2 hold the pins used for the encoder
' Output    : A valid Clockwise or Anti-Clockwise move returns 1 or -1, an invalid move returns 0
' Notes     : None
'
Proc Enc_Read(), SByte   
    Result = 0                                      ' Default to a 0 return value
    Enc_bValidValue = Enc_bValidValue << 2          ' \
    Enc_bValidValue.0 = Enc_Pin1                    ' |
    Enc_bValidValue.1 = Enc_Pin2                    ' | Find the position of the encoder's pins
    Enc_bValidValue = Enc_bValidValue & %00001111   ' /

    If GetBit Enc_wTable, Enc_bValidValue <> 0 Then ' Is the move possibly valid?
        Enc_bStore = Enc_bStore << 4                ' Yes. So shift up by 4-bits
        Enc_bStore = Enc_bStore | Enc_bValidValue   ' Or in the pin positions
        If Enc_bStore = %00101011 Then              ' Is the encoder moving Anti-Clockwise?
            Result = -1                             ' Yes. So return a value of -1
        ElseIf Enc_bStore = %00010111 Then          ' Is the encoder moving Clockwise?
            Result = 1                              ' Yes. So return a value of 1
        EndIf
    EndIf
EndProc

'---------------------------------------------------
' Setup the pins and variables to read a rotary encoder
' Input     : None
' Output    : None
' Notes     : None
'
Proc Enc_Setup()
    Input Enc_Pin1                  ' \ Make sure the encoder pins are inputs
    Input Enc_Pin2                  ' /
    PinPullup Enc_Pin1              ' \ Enable the internal pullup resistors on the encoder pins
    PinPullup Enc_Pin2              ' /
    Enc_bValidValue = 0             ' Reset the valid value for the encoder pins
    Enc_bStore = 0                  ' Reset the stored value for the encoder pins
EndProc

I'm not sure if your compiler has the PinPullup command, but if it does, it will only work with devices that have the WPUx SFRs. It is a new command that comes in very useful with the devices that can enable pull-up resistors per pin. If the PinPullup command is not available on your compiler, look to see if the devcie has the WPUA, WPUB or WPUC etc, SFRs, and if so, they can be written too to enable or disable the internal pull-up resistors, independently for each pin, and replace the PinPullup commands with the WPUB SFR set accordingly for the pins used for the encoder.

In Isis, I couldn't find a rotary encoder, so I used a motor componenent that has a rotary encoder attached to it. The image below is the above program running in the Isis simulator. See the "CW" and "CCW" texts on the serial terminal? these indicate whether the rotary encoder is moving in a particular direction when it increments or decrements the counter variable, for the demo. The 100nF capacitors act as simple switch bounce inhibitors for rotary encoders with physical contacts inside them.

Hopefully, the above program will help some users.
Rotary Encoder.jpg

flosigud

This one uses the Interrupt On Cange:


Quote' -------------------------------------------------------------
' Name : Quadrature.BAS
' Author : Flosi Guðmundssn
' Notice : Copyright (c) 2017 Flosi Guðmundsson
' : All Rights Reserved
' Date : 2.4.2017
' Version : 1.0
' Notes : Interrupt on Change is used to read two quadrature
' : encoders and increment counter. Mainloop sends
' : result serially over Bluetooth to TouchDRO app.
' -------------------------------------------------------------
 Device 18F26K22
 Declare Xtal 80
 Symbol PLLEN = OSCTUNE.6 ' PLL enable
 Symbol PLLRDY = OSCCON2.7 ' PLL run status
    PLLEN=1 ' Enable PLL 4x 16MHz = 64Mhz
    While PLLRDY=0:Wend             ' Wait until PLL is stable
' Include "18f26k22intosc64.Inc"
 Declare Hserial_Baud = 115200
 Declare Reminders = Off
' Declare Optimiser_Level 3
 Declare Dead_Code_Remove = On
 
 On_Hardware_Interrupt Interrupt_handler

 TRISA = %00000000
 TRISB = %11110011 ' Quadrature inputs, Int0, Int1
 TRISC = %00000000

 Symbol GIE = INTCON.7 ' Global Interrupt Enable
 Symbol PEIE = INTCON.6 ' Periferal Interrupt Enable
 Symbol IPEN = RCON.7 ' Interrupt Priorty Enable

 Dim wFSR1 As FSR1L.Word ' Used for look up table

' Setup for Interrupt On Change for PortB 6-7
' ----------------------------------------------------------------------------

 Symbol RBIF = INTCON.0
 Symbol RBIE = INTCON.3

' Setup for Int0
' ----------------------------------------------------------------------------

 Symbol INT0F = INTCON.1
 Symbol INT0IE = INTCON.4
 Symbol INTEDG0 = INTCON2.6
 
' Setup for Int1
' ----------------------------------------------------------------------------

 Symbol INT1F = INTCON3.0
 Symbol INT1IE = INTCON3.3
 Symbol INTEDG1 = INTCON2.5

' PortB pullups
' ------------------------------------------------------------------------

 Symbol NOT_RBPU = INTCON2.7
 IOCB = $F0 ' PortB 7-4 used
 NOT_RBPU = 0 ' PortB pullups on

' Enable interrupts
' ----------------------------------------------------------------------------

 IPEN = 0 ' Interrupt priority OFF
 GIE = 1 ' Global Interrupt enable
 PEIE = 1 ' Enable peripheral enable
 INT0IE = 1 ' Int0 Interrupt Enable
 INT1IE = 1 ' Int1 Interrupt Enable
 RBIE = 1 ' PortB Interrupt Enable

'
' Look up table for incrementing or decrementing the counter
' and pointer to look up table
' ----------------------------------------------------------------------------

 Dim tLookUp[16] As SByte = 0,-1,1,2,1,0,2,-1,-1,2,0,1,2,1,-1,0
 wFSR1 = VarPtr tLookUp

 Dim bNewX As Byte
 Dim bNewZ As Byte
 Dim bOldX As Byte
 Dim bOldZ As Byte

 Dim swCntX As SWord ' values to increment/decrement
 Dim swCntZ As SWord
 Dim sbInc As SByte ' value of increment,0,1,-1

' Dim bTmp As Byte
 Dim fCntReady As Bit
 
 swCntX = 0
 swCntZ = 0

 While 1 = 1
 While fCntReady = 0 : Wend
 fCntReady = 0
 HRSOut "x",SDec swCntX,";","Z",SDec swCntZ,";",10
 Wend

Interrupt_handler:
 Context Save
 If RBIF = 1 Then ' On RBIF interrupt
 RBIF = 0
 Swapf PORTB,WREG ' WREG 3-0 = PORTB 7-4
 bNewZ = WREG
 bNewX = WREG & 3 ' bNewX = PortB 4,5
 Ror bNewZ
 Ror bNewZ
 bNewZ = bNewZ & 3 ' bNewZ = PortB 6,7

 If bNewX <> bOldX Then
 bOldX = bOldX * 4
 WREG = bOldX + bNewX ' Index to table
 sbInc = PLUSW1 ' Get element
 bOldX = bNewX ' update old value
 swCntX = swCntX + sbInc ' change count
 Set fCntReady
 EndIf
 If bNewZ <> bOldZ Then
 bOldZ = bOldZ * 4
 WREG = bOldZ + bNewZ ' Index to table
 swCntZ = swCntZ + PLUSW1
 sbInc = PLUSW1 ' Get element
 bOldZ = bNewZ ' update old value
 swCntZ = swCntZ + sbInc ' change count
 Set fCntReady
 EndIf
 RBIF=0
 EndIf
 If INT0F=1 Then ' Push button X to clear
 INT0F=0
 swCntX = 0
 Set fCntReady
 EndIf
 If INT1F=1 Then ' Push button Z to clear
 INT1F=0
 swCntZ = 0
 Set fCntReady
 EndIf
 Context Restore

Parmin

Quote from: Gamboa on Apr 12, 2021, 10:38 AMI think that inside the ISR you have to delete the interrupt flag "INT0IF: INT0 External Interrupt Flag bit"
INT0IF = 0

You are right, I actually cleared INTF on the program I wrote, I forgot to include it on the snippet.

Quote from: top204 on Apr 12, 2021, 05:30 PMIn Isis, I couldn't find a rotary encoder, so I used a motor componenent that has a rotary encoder attached to it.

Hopefully, the above program will help some users.

This is an awesome substitution.
I will use this in the future.


==========================
Apparently, my problem is not unique, searching the internet shows that a lot of users of similar encoder have the same issue.
The problem is definitely not due to my codes, but due to the fact that direction changes during the pulse would cutoff the previous pulse wave midway and thus step is lost unless the encoder have already output a full wave.

I thank all that offered solution above.
They all works and would all meet similar error with similar encoder.
The problem is definitely hardware origin.
Such is life.



keytapper

There are two method to use. One is to compare the current bit with the previous state. Which might miss some pulse. The second is to combine the bit sequence and compare with the expected pattern. So the latter will give a result after four pulses.
Ignorance comes with a cost

Parmin

Quote from: keytapper on Apr 14, 2021, 06:05 AMThere are two method to use. One is to compare the current bit with the previous state. Which might miss some pulse. The second is to combine the bit sequence and compare with the expected pattern. So the latter will give a result after four pulses.

Thank you for the reply.

In either of your solutions, step will still be lost when the direction state change in mid pulse or pulse pattern since the output is incomplete and this not a valid input as far as the PIC code concerned

It seems that the only solution to eliminate pulse loss is to use an expensive absolute encoder rather than the cheaper incremental encoder.

John Drew

I use the AS5045 chip absolute encoder chip and a special magnet with 2 poles on the same side (forgot the correct name). It can provide accurate 12 bit results at 1000 + RPM.
Works a treat although I only spin 1 rev per two minutes in my application.
John

See_Mos

#13
to put it simply, manual pulse encoders are not expected to be 100% accurate.

I have used the method posted by Mapo and that is about as good as it gets.  Detecting edges on both A and B means there is less chance of error and a faster PIC helps as well.

As John wrote, if you want accuracy you need an absolute encoder.

top204

#14
I used one of those types of device many years ago when they were first available, and I was experimenting with Galvanometers for laser scanning. They were like "magic". :-)

The magnet spins around above the top of the chip and the, almost, perfect positon of the magnet is read from the chip. I still find it extremely clever, and a use of hall sensing that is phenomenal.

No friction and no bouncing and the position of a motor is known almost immediately, which is what was required for the laser scanning, but without all of the extra components required for capacitive or optical positioning that is normally used. :-)


Giuseppe MPO

Quote from: See_Mos on Apr 14, 2021, 09:21 AMAs John wrote, if you want accuracy you need an absolute encoder.

It is absolutely not true,
I used a PIC with CIP quadrature (eg PIC18F2431) and after many tests
it never lost a step, even doing x4 readings.

Stephen Moss

Quote from: Parmin on Apr 14, 2021, 07:56 AMIn either of your solutions, step will still be lost when the direction state change in mid pulse or pulse pattern since the output is incomplete and this not a valid input as far as the PIC code concerned
I am not sure what you mean by that, the encoder outputs a repeating sequence of 4 states (condition, pulses whatever you want to call them) 00, 01, 11, 10.
How long each state is active for depends on how fast the encoder rotates. The direction state cannot change "mid pulse" as the length of the pulse is not fixed, it could shorten the duration of a state (pulse) but all things being equal it would take as long for the state to change when a change of direction is made as from one to another in the same direction due to inertia and hysteresis (more so when a Schmitt trigger input is used) as a change of direction and therefore state will not be instantaneous.

You may be correct in what you say, however if it is being turned manually I would be surprised if that were the case, I have controlled a 3 phase motor reading a 3 bit grey code output on the shaft to commutate the phases, that is 12 commutations per revolution at 4000rmp which I believe results in a encoder state (pulse width) of 1.25ms.
I am not going to claim with absolute certainty that it sees every one as I have not looked through mountains of data to check that but if your encoder has 100 pulses per revolution and someone manages to manually turn it at 60 RPM, if my math is correct that is a minimum pulse width of 10mS. When you PIC is executing one instruction every 50nS (80MHz) I seems unlikely that it would be too slow to read a state much shorter than that (i.e. 10uS = 200 instructions) unless something in you code is blocking it from doing so such as a ISR that takes too long to execute, not clearing the interrupt Flag to detect the next interrupt or the change occurring while interrupts are disabled (i.e. another ISR is being executed).

Hence my original reply, perhaps this way makes it clearer, in the code you originally posted...
1) change from 00 to 01 - no interrupt as A remains low (this change is missed as no detection of change on B)
2) change from 01 to 11 - interrupt as A goes high (this change is detected)
3) change from 11 to 10 - no interrupt as A is already high (this change is missed as no detection of change on B & stuck in IRS while loop)
4) change from 10 to 00 - no interrupt as A goes low and change occurs while in the ISR (this change is missed)
Additionally, if you change from point 1 to point 2, triggering the interrupt and then reverse direction back to point 1, the while loop in your ISR means that the change occurs while still in your ISR and so can never be detected even if you have IOC active on both A & B.
If we go in the other direction...
1) change from 00 to 10 - interrupt as A goes high (this change is detected)
2) change from 10 to 11 - no interrupt as A remains high (this change is missed as no detection of change on B)
3) change from 11 to 01 - no interrupt as A goes low and change occurs while in the ISR (this change is missed)
4) change from 01 to 00 - no interrupt as A remains low (this change is missed as no detection of change on B)

I am not sure what you are trying to achieve but as far a I can see you original code is only going to detect one state in 4 so you location count is going to be so inaccurate as to the practically useless. Try some of the other solutions already provided.


John Drew

I've used many encoders over the years. I use them for positioning antennas for satellite and moon tracking work.
If you're interested have a look on my website http://vk5dj.com
Look under projects/beam controller/ then the various sub headings.
My favourites are Georg (DF1SR) hh-12 azimuth and his newly manufactured elevation sensor which simply clamps on an antenna's boom. They are reasonably priced and well made.
A quadrature encoder is not suitable for my work as they lose position when the power is lost without special power down saving. I'm not saying quadrature encoders don't have their place e.g tuning or volume controls. Just not for pointing a 10m dish at the moon in a repeatable way.
John

Parmin

#18
@Stephen Moss

First of all the encoder IS being turned MANUALLY, this is the reason for the encoder in the first place, ie. to adjust the position of a stepper motor to reflect the position on the encoder wheel.

OTOH, I would try your suggestion and see if it would actually improve the step retention.
Redoing the interrupt from INT to IOC to capture the state of INPUTA and INPUTB should keep track of every possible signal output from the encoder regardless of the state of change within the waveform and thus should not lose the step at any time.

I will rewrite my code and get back once I tried it out.

@John Drew
would it not be possible to save the last position on EEPROM?


okmn

This work has been done in c language, but i hope  it will be useful to you, at least to see a different semantic structure.


Quadrature Decoder with PIC18F16Q40
https://github.com/microchip-pic-avr-examples/pic18f16q40-quadrature-decoder