Shop OBEX P1 Docs P2 Docs Learn Events
PropBASIC routine for quadrature encoder reading....FAST and worked first time! :) — Parallax Forums

PropBASIC routine for quadrature encoder reading....FAST and worked first time! :)

MicksterMickster Posts: 2,698
edited 2012-10-30 06:05 in Propeller 1
Posted this a while ago in another quadrature encoder thread as an alternative to using pure PASM.
Well I just got around to testing it and it required no debug at all (a FIRST for me)!!!

From what I can tell of the generated PASM code, this method appears to be using less than 10 instructions at any time.

DEVICE P8X32A, XTAL1, PLL16X
FREQ 80_000_000

Chan_A PIN 0 INPUT
Chan_B PIN 1 INPUT
Chan_Z PIN 2 INPUT

Counter VAR LONG = 0
Idx_Monitor VAR LONG = 0
Idx_Count VAR LONG = 0
Count_Error VAR LONG = 0

PROGRAM Start

Start:
Watch Counter   ' This is for ViewPort

'Decide where to start
If Chan_A = 1 And
    Chan_B = 1 Then
    Goto A1B1
Endif

If Chan_A = 0 And
    Chan_B = 0 Then
    Goto A0B0
Endif

If Chan_A = 1 And
    Chan_B = 0 Then
    Goto A1B0
Endif

If Chan_A = 0 And
    Chan_B = 1 Then
    Goto A0B1
Endif

'Should keep hopping between these labels unless an illegal 
'condition crops up.
  
 A1B1:
 If Chan_A = 0 Then
   Dec Counter
   Goto A0B1
 Elseif Chan_B = 0 Then
   Inc Counter
   Goto A1B0
 Endif
 Goto A1B1
 
 A0B0:
 If Chan_A = 1 Then
   Dec Counter
   Goto A1B0
 Elseif Chan_B = 1 Then
   Inc Counter
   Goto A0B1
 Endif
 Goto A0B0
 
 A1B0:
 If Chan_A = 0 Then
   Inc Counter
   Goto A0B0
 Elseif Chan_B = 1 Then
   Dec Counter
   Goto A1B1
 Endif
 Goto A1B0
 
 A0B1:
 If Chan_A = 1 Then
   Inc Counter
   Goto A1B1
 Elseif Chan_B = 0 Then
   Dec Counter
   Goto A0B0
 Endif
 Goto A0B1
End


Regards,

Mickster

Comments

  • PublisonPublison Posts: 12,366
    edited 2012-10-29 09:05
    Missed this one yesterday. Thanks for that!
  • VonSzarvasVonSzarvas Posts: 3,491
    edited 2012-10-29 13:56
    Yes, well done and thank you for posting the code.
    Hopefully soon I will find time for quadrature play, and now here is a great starting point.

    :)
  • MicksterMickster Posts: 2,698
    edited 2012-10-30 06:05
    I figure this code to be good for at least 1.5M quadrature counts/sec even when I account for the worst case of 22 clocks required for writing to the hub. So, for my current application, I have plenty of clocks to add other functionality such as filtering and also adding support for the three differential encoder outputs as well.

    Another cool trick, assuming the application involves full rotations of the encoder, is to employ the index pulse as a means of checking the integrity of the count. The differnce between one "Idx_Count", below and the next, should always be a multiple of the encoder's quadrature count resolution (2000 in my case). Typically the index pulse goes low when ChA and ChB are low, so:

    A0B0:    ' A and B are in the low state
    If Chan_A = 1 Then
       Dec Counter
       Goto A1B0
    Elseif Chan_B = 1 Then
       Inc Counter
       Goto A0B1
    Endif
    If Chan_Z = 0 Then   'If the Index is also low, capture the current encoder count
       Idx_Count = Counter
    Endif
    Goto A0B0
    
    

    Cheers.

    Mickster
  • I have used the cheap rotary encoders for HMI (i.e. tuning a digital radio), but my code had some slop that was not always reliable. I took the liberty of squeezing this into a TASK and it seems to be rock solid. Not sure if this would be helpful for anyone that is looking to have this data update in the background.

    I may still add a few lines to monitor for switch presses and set a hub variable.
    ' --- stuff that needs to be defined in advance ---
    hub_Count   HUB BYTE = 0
    ENCODER     TASK    AUTO    ' update status of switch and counter
    
    ' ------------------------------------------------------------------------
    ' Convert X4 encoding into X1 counter
    ' Report counter to hub variable, bound to valid range of 0-31
    ' ------------------------------------------------------------------------
    TASK ENCODER
        EN0AB       PIN 1..0    INPUT       ' Pin Group
        EN0A        PIN   1     INPUT       ' Rotary Encoder #0, output A
        EN0B        PIN   0     INPUT       ' Rotary Encoder #0, output B
        
        Counter         VAR LONG = 0        ' Calculate X1 encoding
        CounterX4       VAR LONG = 0        ' Read X4 encoding 
        EN0_State       VAR LONG = 0        ' Read pin group state
    
      ' Define Subroutines, Functions
        Update_Count    SUB 0
    
    
        EN0_State = EN0AB
        BRANCH EN0_State, A0B0, A0B1, A1B0, A1B1    ' finite states = 00, 01, 10, 11
    
    
    ' --- From known initial state, iteratively update pin state and counter
    
        A1B1:
        If EN0A = 0 Then
            Dec CounterX4
            Update_Count
            Goto A0B1
        Elseif EN0B = 0 Then
            Inc CounterX4
            Update_Count
            Goto A1B0
        Endif
        Goto A1B1
         
        A0B0:
        If EN0A = 1 Then
            Dec CounterX4
            Update_Count
            Goto A1B0
        Elseif EN0B = 1 Then
            Inc CounterX4
            Update_Count
            Goto A0B1
        Endif
        Goto A0B0
     
        A1B0:
        If EN0A = 0 Then
            Inc CounterX4
            Update_Count
            Goto A0B0
        Elseif EN0B = 1 Then
            Dec CounterX4
            Update_Count
            Goto A1B1
        Endif
        Goto A1B0
         
        A0B1:
        If EN0A = 1 Then
            Inc CounterX4
            Update_Count
            Goto A1B1
        Elseif EN0B = 0 Then
            Dec CounterX4
            Update_Count
            Goto A0B0
        Endif
        Goto A0B1
    
      ' *** --------------- SUBROUTINES INSIDE TASK UPDATE_LCD -------------- ***  
      ' Counter range is 0-31 (0 to 32-1)
      ' CounterX4 range is 0-127 (0 to (32*4)-1)
    SUB Update_Count       
        CounterX4 = CounterX4 MAX 127
        CounterX4 = CounterX4 MIN 0
        Counter = CounterX4>>2
        WRBYTE hub_Count, Counter
    ENDSUB ' Update_Count
        
    
    ENDTASK ' ENCODER
    
  • ErNaErNa Posts: 1,752
    The cheap HMI encoders often have no equidistant switching points, in this case it makes sense to count by two for one count. That makes the switching more reliable
  • Ahhhh...PropBasic. Code doesn't get any cleaner than this :smile:

    Disclaimer: Have yet to try FlexBASIC though (time constraints)
  • A pretty straightforward FlexBASIC port of that code would look something like:
    ' X1 encoding as a global 8 bit variable
    dim hub_CountX1 as ubyte
    
    ' update the X1 encoding from the X4 encoding
    ' this is called a lot so put it in COG memory even for LMM
    function for "cog" Update_Count(countX4)
      if countX4 > 127 countX4 = 127
      if countX4 < 0 countX4 = 0
      hub_CountX1 = countX4 >> 2
      return countX4
    end function
    
    ' Rotary Encoder #0, output A
    #define EN0A  input(1)
    ' Rotary Encoder #0, output B
    #define EN0B  input(0)
    ' Both pins
    #define EN0AB input(1, 0)
    
    sub encoder
      var CounterX4 = 0    ' Read X4 encoding
      var en0_state = 0    ' read pin group state
    
      en0_state = EN0AB
      on en0_state goto A0B0, A0B1, A1B0, A1B1
    
    ' --- From known initial state, iteratively update pin state and counter
    
    A1B1:
        if EN0A = 0 then
          CounterX4 = Update_Count(CounterX4-1)
          goto A0B1
        else if EN0B = 0 then
          CounterX4 = Update_Count(CounterX4+1)
          goto A1B0
        endif
        goto A1B1
        
    A0B0:
        if EN0A = 1 then
          CounterX4 = Update_Count(CounterX4-1)
          goto A1B0
        else if EN0B = 1 then
          CounterX4 = Update_Count(CounterX4+1)
          goto A0B1
        endif
        goto A0B0
        
    A1B0:
        if EN0A = 0 then
            CounterX4 = Update_Count(CounterX4+1)
            Goto A0B0
        else if EN0B = 1 then
            CounterX4 = Update_Count(CounterX4-1)
            Goto A1B1
        endif
        goto A1B0
         
    A0B1:
        if EN0A = 1 then
            CounterX4 = Update_Count(CounterX4+1)
            goto A1B1
        else if EN0B = 0 then
            CounterX4 = Update_Count(CounterX4-1)
            goto A0B0
        endif
        goto A0B1
    end sub
    
    '' run the encoder program
    encoder
    
  • ErNaErNa Posts: 1,752
    wrote long ago this in SPIN:
    NxtState :=  ina[PinB]<<1 + ina[PinA]
          case EncState
         
            %00:  case  NxtState
                    %01 : count ++
                    %10 : count --
        
            %01:  case  NxtState
                    %11 : count ++
                    %00 : count --
        
            %11:  case  NxtState
                    %10 : count ++
                    %01 : count --
        
            %10:  case  NxtState
                    %00 : count ++
                    %11 : count --
          LONG[ButtCounPtr] := count   ' copy counter to the global memory
          EncState := NxtState
    

    I don't know it there is a case in BASIC
  • Hi- Love it all

    But where is Heater???
    He would have a fit!!!
    all those goto's -argh!
    call yourselves a programmers....

    Dave
  • @tritonium
    The thing is that; the goto translates directly to a PASM jmp.

    My objective was to see how few clock cycles I could get away with.

    Yes, BASIC has case but the above SPIN example, even if it was written in 'c' uses too many clock cycles (for me).



  • frank freedmanfrank freedman Posts: 1,983
    edited 2020-06-01 02:20
    But isn't case just a fancy goto? Jmp and all its relations as well? IIRC, wasnt the goto that was evil, rather its indiscriminate use in creating obfuscation a perl hack would be proud of.....

    And how consice the spin version. Would love to see the clock counts of all of these versions compared.
  • MicksterMickster Posts: 2,698
    edited 2020-06-01 05:40
    But isn't case just a fancy goto? Jmp and all its relations as well? IIRC, wasnt the goto that was evil, rather its indiscriminate use in creating obfuscation a perl hack would be proud of.....
    It's been eight years but I seem to remember trying a few options which always resulted in extra overhead. I let PropBasic do its thing, thinking that I could then optimize the generated PASM but there was nothing to optimize.

    And how consice the spin version. Would love to see the clock counts of all of these versions compared.

    :lol:

    Client: Hey, this machine is way too slow...this production rate is gonna kill me!
    Me: Yeah, I know but the code is really pretty.
  • Tracy AllenTracy Allen Posts: 6,664
    edited 2020-06-03 21:15
    In PBASIC, I'd used a couple of shorthand instructions. One a table lookup...
    decode:
      new = inA & %11.  ' read encoder, two bits
      lookup old<<2+new, [0,1,-1,0,-1,0,0,1,1,0,0,-1,0,-1,1,0], xx
      myCount = myCount + xx
      old = new
    
    One boolean...
    old VAR nib
    new VAR nib
    ob VAR old.bit0 ' bits to test
    nb VAR new.bit1
    decode:
     new=inC & %11 ' read encoder, two bits
     if new<>old then  ' skip if no change
        myCount = ob^nb << 1 - 1+ myCount ' calculate motion, add 1 for CW, -1 for CCW
        old=new ' new becomes old   
    
    The boolean form is readily extended to multiple encoders processed in parallel.
  • To perform the X1 encoding in my TASK, you can theoretically perform this in a much more elegant fashion. Very fast, too.
    WAITPEQ 0, EN0A         ' EN0 A falling edge
        idx = EN0B              ' variable required to read the current state of a pin
    
        IF idx = 1 THEN                       ' test if increment
            INC counter                      
        ELSE                                  ' decrement, since not increment
            DEC counter                       
        ENDIF
    

    However, I experienced intolerable failures. Counting would go the wrong way about 10% of the time due to switch noise. My rest of my code relied on the encoder coming to rest in the detent position (1,1), but sometimes the encoder did not.

    My original application was just tuning an antique-style radio. So, having to fiddle with the knob to get it to tune is a real touch of authenticity. This wasn't a feature that I was hoping for in future applications.

  • In PBASIC, I'd used a couple of shorthand instructions. One a table lookup...
    decode:
      new = inA & %11.  ' read encoder, two bits
      lookup old<<2+new, [0,1,-1,0,-1,0,0,1,1,0,0,-1,0,-1,1,0], xx
      myCount = myCount + xx
      old = new
    
    One boolean...
    decode:
     new=inC & %11 ' read encoder, two bits
     if new<>old then  ' skip if no change
        myCount = ob^nb << 1 - 1+ myCount ' calculate motion, add 1 for CW, -1 for CCW
        old=new ' new becomes old   
    
    The boolean form is readily extended to multiple encoders processed in parallel.

    Oh absolutely. It might seem a ridiculous waste to dedicate a cog to a single axis but I really do have many hundreds of 6,000 RPM motors out there with 4096 line (16384 quad count) encoders.
    At some point, these machines will require a new control system and so I have been experimenting with various possible solutions.
  • hatallica wrote: »
    To perform the X1 encoding in my TASK, you can theoretically perform this in a much more elegant fashion. Very fast, too.
    WAITPEQ 0, EN0A         ' EN0 A falling edge
        idx = EN0B              ' variable required to read the current state of a pin
    
        IF idx = 1 THEN                       ' test if increment
            INC counter                      
        ELSE                                  ' decrement, since not increment
            DEC counter                       
        ENDIF
    

    However, I experienced intolerable failures. Counting would go the wrong way about 10% of the time due to switch noise. My rest of my code relied on the encoder coming to rest in the detent position (1,1), but sometimes the encoder did not.

    My original application was just tuning an antique-style radio. So, having to fiddle with the knob to get it to tune is a real touch of authenticity. This wasn't a feature that I was hoping for in future applications.

    I have yet to experience the detented encoders but I have heard of glich/debouce issues, elsewhere. I need to grab some to have a play with :smile:
  • tritonium wrote: »
    Hi- Love it all

    But where is Heater???
    He would have a fit!!!
    all those goto's -argh!
    call yourselves a programmers....

    Dave

    He already did have a fit.

    A couple years ago I challenged him on four lines of BASIC quadrature code. He turned it into a page of GOTO-less C that ran twice as low.

    But my hilarious favorite has always been this one: The 'Jeopardy' switch. Basically, if one contestant hits the button first, it has to lock out the other contestant's buttons.

    This could be done with 2 to 4 logic gates, or four lines of BASIC. Instead, someone tried to do it in 1.5 pages of Blockly, and I think STILL COULDN'T GET IT TO WORK:

    https://forums.parallax.com/discussion/168171/jeopardy-style-game-control
  • Mickster wrote: »
    I have yet to experience the detented encoders but I have heard of glich/debouce issues, elsewhere. I need to grab some to have a play with :smile:

    I have a pile of the encoders (CS-CO043), but it would have been easier to use the KY-040 modules that are readily available. When Goldmine Electronics had some encoders that appear to be Alps EC11E, I loaded up on more. I ended up with a lifetime supply of encoders (and laid out 2 PCBs to play with them).

    There are opportunities to optimize the TASK for my purpose - perhaps some version of Tracy Allen's boolean example that frees up the cog to also monitor switch state. For HMI applications, I would struggle to get more than 25 detent clicks (100 state changes) in a second. So, checking the switch, updating hub variables, etc. could certainly be done even with LMM code in a TASK.
  • MicksterMickster Posts: 2,698
    edited 2020-06-03 09:17
    @hatallica

    frees up the cog to also monitor switch state. For HMI applications, I would struggle to get more than 25 detent clicks (100 state changes) in a second. So, checking the switch, updating hub variables, etc. could certainly be done even with LMM code in a TASK.

    Something like this could be added to what you have(?)
    'Decide where to start
    If Chan_A = 1 And
        Chan_B = 1 Then
        EncState=1							'Goto A1B1
    Endif
    
    If Chan_A = 0 And
        Chan_B = 0 Then
        EncState=2							'Goto A0B0
    Endif
    
    If Chan_A = 1 And
        Chan_B = 0 Then
        EncState=3							'Goto A1B0
    Endif
    
    If Chan_A = 0 And
        Chan_B = 1 Then
        EncState=4							'Goto A0B1
    Endif
    
    TskB=1'Initialize pointer for TaskB
    
    DO
      
      on EncState goto A1B1,A0B0,A1B0,A0B1
      
    END 'uh-oh, we fell through...WTF!
    
     A1B1:
     If Chan_A = 0 Then
       Dec Counter
       EncState=4
     Elseif Chan_B = 0 Then
       Inc Counter
       EncState=3
     Endif
     Goto TaskB
     
     A0B0:
     If Chan_A = 1 Then
       Dec Counter
       EncState=3
     Elseif Chan_B = 1 Then
       Inc Counter
       EncState=4
     Endif
     Goto TaskB
     
     A1B0:
     If Chan_A = 0 Then
       Inc Counter
       EncState=2
     Elseif Chan_B = 1 Then
       Dec Counter
       EncState=1
     Endif
     Goto TaskB
     
     A0B1:
     If Chan_A = 1 Then
       Inc Counter
       EncState=1
     Elseif Chan_B = 0 Then
       Dec Counter
       EncState=2
     Endif
    
    TaskB:
    
    'Do other stuff here.
    'I typically use more state-machine structures
    
    on TskB goto TB1,TB2,TB3,TB4
    
    TB1: 'Set an output and wait for an input
    		'IF [condition satisfied] Then
    			inc TskB
    		'Endif
    		'Goto TaskC
    TB2: 'Set an output and wait for an input
    		'IF [condition satisfied] Then
    			inc TskB
    		'Endif
    		'Goto TaskC
    TB3: 'Set an output and wait for an input
    		'IF [condition satisfied] Then
    			inc TskB
    		'Endif
    		'Goto TaskC
    TB4: 'Set an output and wait for an input
    		'IF [condition satisfied] Then
    		'TskB=1
    		'Endif
    
    
    
    TaskC: 'etc., etc. 
    LOOP 
    
    

    Edit: Even using LMM, we are still looking at a worst-case of four million instructions/second, right?
  • @"frank freedman"
    Would love to see the clock counts of all of these versions compared.

    Hi Frank. This is pretty much what's happening at any one time. I don't have my PASM reference
    but I believe that we need to allow 22 clocks for the hub write(?)
    PropBasic output						'PropBasic source
    
    
    A1B1                                                                ' A1B1:
        and Chan_A,ina WZ, NR		                ' If Chan_A = 0 Then
        IF_NZ jmp #__ELSE_5                   
    
        subs Counter,#1			                       'Dec Counter
        wrlong	Counter,__HubCTR_adr	               'Wrlong HubCTR,Counter
        jmp #A0B1				                       'Goto A0B1
        jmp #__ENDIF_5			                       'Elseif Chan_B = 0 Then
    __ELSE_5                                                    
        and Chan_B,ina WZ, NR           
        IF_NZ jmp #__ELSE_6                   
        adds Counter,#1			                      'Inc Counter
        wrlong Counter,__HubCTR_adr	              'Wrlong HubCTR,Counter
        jmp #A1B0			                              'Goto A1B0
    __ELSE_6							      'Endif
    __ENDIF_5                                                   
       jmp #A1B1						      'Goto A1B1
    
    
  • I realized I didn't define the variables in the snippets I posted above. In PBASIC, old and new are nibbles, with the encoder state in bit0 and bit1.
    old VAR nib
    new VAR nib
    ob VAR old.bit0 ' bit zero of the old state
    nb VAR new.bit1 ' compared to bit 1 of the new state
    
    those two bits settle the direction of rotation, CW or CCW
    
    0     1    3     2     0  state
    ----> ----> ----> ---->    CW
     
    0  _  1  _  1  _  0  _  0  new
      /     /     /     /                all 0->1 or 1->0
    0  -  0  -  1  -  1  -  0  old 
     
     
     
     
    0     1     3     2     0   state
     <---- <---- <---- <----    CCW
     
    0  _  1  _  1  _  0  _  0   new
        \     \     \     \      all 0->0 or 1->1
    0  -  0  -  1  -  1  -  0   old
    

    Jeff Martin's Quadrature Encoder.spin (pasm cog) uses the boolean logic method, extended to up to 16 encoders in parallel.
    Oh absolutely. It might seem a ridiculous waste to dedicate a cog to a single axis but I really do have many hundreds of 6,000 RPM motors out there with 4096 line (16384 quad count) encoders.

    Indeed!
  • Yeah, I was wondering where the ob and nb were coming from :smile:
  • Jeff Martin's Quadrature Encoder.spin (pasm cog) uses the boolean logic method, extended to up to 16 encoders in parallel.

    This is a very nice piece of work and I have a project in mind for it. The only downside to using it with one or two encoders is that it has a fixed overhead of 150 clocks.
Sign In or Register to comment.