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

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.
Regards,
Mickster
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
Hopefully soon I will find time for quadrature play, and now here is a great starting point.
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 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
Disclaimer: Have yet to try FlexBASIC though (time constraints)
' 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
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
But where is Heater???
He would have a fit!!!
all those goto's -argh!
call yourselves a programmers....
Dave
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).
And how consice the spin version. Would love to see the clock counts of all of these versions compared.
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.
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.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.
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.
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
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
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.
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?
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
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.
Indeed!
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.