Shop OBEX P1 Docs P2 Docs Learn Events
FWIW Gray Code state machine/truth table with self modifying code — Parallax Forums

FWIW Gray Code state machine/truth table with self modifying code

pedwardpedward Posts: 1,642
edited 2015-11-22 04:43 in Propeller 1
Hi all, I was reading an article on quadrature encoders and decided to make a spreadsheet that determines the direction of a gray code encoder. I looked at the PID code that Kwabena W. Agyeman wrote for reading encoders and thought I'd share this bit. The reason I'm rehashing a topic is that I think encoder calculations can be made faster with my method.

Here is the table, I had to make it a link because the forum migration totally borked tables.

apsoft.com/~pedward/graycode_table.html

I will attach the spreadsheet too.

The current code in K's object is:
                        mov     buffer,           ina                      ' Sample left and right inputs.
                        test    buffer,           leftEncoderPin wc        '
                        test    buffer,           rightEncoderPin wz       '
                        muxc    encoderCurrent,   #2                       ' Store left and right inputs.
                        muxnz   encoderCurrent,   #1                       '
initializeOnce          mov     encoderPrevious,  encoderCurrent           ' Initializes previous state once.
                        mov     initializeOnce,   #0                       '
                        cmp     encoderPrevious,  encoderCurrent wz        ' Update current state.
if_nz                   rev     encoderPrevious,  #30                      '
if_nz                   xor     encoderPrevious,  encoderCurrent           '
if_nz                   cmpsub  encoderPrevious,  #2 wc, nr                '
if_nz                   sumc    encoderPosition,  #1                       '
                        wrlong  encoderPosition,  positionAddress          '
                        mov     encoderPrevious,  encoderCurrent           ' Update previous state. 

My proposed code:
' previous in low half of nibble and current in high half of nibble
'             0 1  2 3  4 5 6 7 8 9 A  B C  D E F
GrayCode long 0,1,-1,0,-1,0,0,1,1,0,0,-1,0,-1,1,0

                         mov     buffer,           ina                      ' Sample left and right inputs.
                         test    buffer,           leftEncoderPin wc        '
                         test    buffer,           rightEncoderPin wz       '
                         muxc    update,   #8                       ' Store left and right inputs.
                         muxnz   update,   #4                       '

update                   adds encoderPosition, GrayCode    'self modifying code
                         muxc    update,   #2                       ' Store previous value
                         muxnz   update,   #1                       '
                         wrlong  encoderPosition,  positionAddress          '
«1

Comments

  • KyeKye Posts: 2,200
    edited 2011-11-03 16:20
    You should move this to the propeller forum! There is much more activity over there.

    I like the table based idea. Much better!

    Thanks,
  • pedwardpedward Posts: 1,642
    edited 2011-11-03 17:07
    This method works with stepper motors too. I made a bipolar stepper driver in SPIN that uses a lookup table to drive the phases. One of the challenges of driving steppers is keeping track of the last and next step values when stopping, starting, and changing direction. Since a bipolar stepper motor is 4 bits, I made a lookup table that uses the current port reading to lookup the next port output.

    The Logic is like this:

    A simple wave drive stepper waveform would be:

    0001
    0010
    0100
    1000

    If you index that into an array, you get:

    Curr Next
    0001 0010
    0010 0100
    0100 1000
    1000 0001

    I omitted the other entries, but you end up with a 16 entry table of 4 bits each. You use the current port value as the index to read the next port value. I used pointers for direction and had an up and down table.

    After the Gray code exercise last night, I thought about making a TTL gray code counter :) but alas the 7400 competition is over! :frown:
  • kuronekokuroneko Posts: 3,623
    edited 2011-11-03 22:06
    @pedward: In order to make your proposed code work you should move the muxc update, #2 before the adds otherwise the previous muxnz is ignored. Meaning if an insn modification should have immediate effect you'll need at least one (usually) unrelated insn between the last modification and the modified insn. This is because the next insn is fetched one cycle before the current insn writes its result. Also, the table needs to be 16n aligned which isn't always guaranteed.
  • pedwardpedward Posts: 1,642
    edited 2011-11-03 22:55
    I understand the issue with the muxc update, #2, the cog is prefetching instructions. I would assume the muxc update, #2 *DOES* update after the instruction fetch, just like I want it to. You could use ORG and place the GrayCode table at the beginning of COG ram, I just wrote up a short snippet and dumped the compiler output and the table was located at 0000-0010.

    Here is a chunk of sample code I ran through the compiler:
    [FONT=courier new]DAT
    
    ORG 0
    ' previous in low half of nibble and current in high half of nibble
    '             0 1  2 3  4 5 6 7 8 9 A  B C  D E F
    GrayCode long 0,1,-1,0,-1,0,0,1,1,0,0,-1,0,-1,1,0
    positionAddress long    0
    encoderPosition long    0
    leftEncoderPin          long    0
    rightEncoderPin         long    0
    buffer  res  1
    
    init    mov             buffer,           ina                      ' Sample left and right inputs.
            test            buffer,           leftEncoderPin wc        '
            test            buffer,           rightEncoderPin wz
            muxc            update,   #8                       ' Store left and right inputs.
            muxnz           update,   #4                       '
            muxc            update,   #2                       ' Store previous value
    
    update  adds            encoderPosition, GrayCode    'self modifying code
            muxnz           update,   #1                       '
            wrlong          encoderPosition,  positionAddress
            jmp             init          '[/FONT]
    

    Here is the corresponding compiler output:
    [FONT=courier new]|===========================================================================|
    Object DAT Blocks
    |===========================================================================|
    0018(0000)             | ORG 0
    0018(0000) 00 00 00 00 | GrayCode long 0,1,-1,0,-1,0,0,1,1,0,0,-1,0,-1,1,0
    001C(0001) 01 00 00 00 | 
    0020(0002) FF FF FF FF | 
    0024(0003) 00 00 00 00 | 
    0028(0004) FF FF FF FF | 
    002C(0005) 00 00 00 00 | 
    0030(0006) 00 00 00 00 | 
    0034(0007) 01 00 00 00 | 
    0038(0008) 01 00 00 00 | 
    003C(0009) 00 00 00 00 | 
    0040(000A) 00 00 00 00 | 
    0044(000B) FF FF FF FF | 
    0048(000C) 00 00 00 00 | 
    004C(000D) FF FF FF FF | 
    0050(000E) 01 00 00 00 | 
    0054(000F) 00 00 00 00 | 
    0058(0010) 00 00 00 00 | positionAddress long    0
    005C(0011) 00 00 00 00 | encoderPosition long    0
    0060(0012) 00 00 00 00 | leftEncoderPin          long    0
    0064(0013) 00 00 00 00 | rightEncoderPin         long    0
    0068(0014)             | buffer  res  1
    0068(0015) F2 29 BC A0 | init    mov             buffer,           ina                      ' Sample left and right inputs.
    006C(0016) 12 28 3C 61 |         test            buffer,           leftEncoderPin wc        '
    0070(0017) 13 28 3C 62 |         test            buffer,           rightEncoderPin wz
    0074(0018) 08 36 FC 70 |         muxc            update,   #8                       ' Store left and right inputs.
    0078(0019) 04 36 FC 7C |         muxnz           update,   #4                       '
    007C(001A) 02 36 FC 70 |         muxc            update,   #2                       ' Store previous value
    0080(001B) 00 22 BC D0 | update  adds            encoderPosition, GrayCode    'self modifying code
    0084(001C) 01 36 FC 7C |         muxnz           update,   #1                       '
    0088(001D) 10 22 3C 08 |         wrlong          encoderPosition,  positionAddress
    008C(001E) 15 00 3C 5C |         jmp             init          '
    [/FONT]
    

    Thanks for the feedback. This is the first PASM I've written, I learned ASM on the x86 about 17 years ago.
  • kuronekokuroneko Posts: 3,623
    edited 2011-11-03 23:06
    pedward wrote: »
    You could use ORG and place the GrayCode table at the beginning of COG ram, I just wrote up a short snippet and dumped the compiler output and the table was located at 0000-0010.
    While that's trueA, the issue here is that cog code execution always starts at 0 (provided cognew/coginit is used). So while table entries like 0 and 1 are treated as nops, -1 is considered a form of waitvid which simply blocks when the video h/w isn't setup. So either you jump over the table (and data) and replace the first entry or you simply put the table after the code.

    Also, res should only be used at the end of the code. And while we are here, in your case you want jmp #init (direct jump).


    A not quite, org doesn't relocate stuff for you, it just messes with the address reference for the assembler, i.e. even if you start a fragment with org 100 and pass it to cognew you usually don't get what you expect (as the first insn still ends up at address 0)
  • KyeKye Posts: 2,200
    edited 2013-03-10 12:52
    Hey Pedward,

    Here's a good way to build the table:
                            org     0
    
    
                            { Gray Code Direction Table
                            
                                         Prev
                                    00  01  10  11    
                              Curr   
                               00   +0, +1, -1, +0
                               01   -1, +0, +0, +1
                               10   +1, +0, +0, -1 
                               11   +0, -1, +1, +0
                            
                              Thanks Pedward }
    
    
                            mov     $,                          #0 ' +0
                            mov     $,                          #1 ' +1
                            neg     $,                          #1 ' -1
                            mov     $,                          #0 ' +0
    
    
                            neg     $,                          #1 ' -1
                            mov     $,                          #0 ' +0
                            mov     $,                          #0 ' +0
                            mov     $,                          #1 ' +1
    
    
                            mov     $,                          #1 ' +1
                            mov     $,                          #0 ' +0
                            mov     $,                          #0 ' +0
                            neg     $,                          #1 ' -1
                            
                            mov     $,                          #0 ' +0
                            neg     $,                          #1 ' -1
                            mov     $,                          #1 ' +1
                            mov     $,                          #0 ' +0
    

    Thanks,
  • MagIO2MagIO2 Posts: 2,243
    edited 2013-03-10 23:41
    ... good way ... hmmm ...

    ;o)

    It is a way, but for me it's more like a hack than a good way. What is the benefit of running code vs. setting up the values directly? The extra waste of runtime? What's the problem putting the data at the end of the code?
  • kuronekokuroneko Posts: 3,623
    edited 2013-03-11 00:09
    MagIO2 wrote: »
    What's the problem putting the data at the end of the code?
    The code currently requires 16n table alignment. Are you going to suggest we should waste some longs due to re-alignment?
  • pedwardpedward Posts: 1,642
    edited 2013-03-11 01:40
    I like the technique, it's elegant! I have implemented the algorithm in the original post in C for the HC08, I will post it when I'm done. The application is an encoder co-processor, much like a mouse. I'm working on the serial communication now. I'd like to do SPI, but it doesn't have open collector type outputs, so I probably have to emulate that by switching from output to input to de-assert the pin.
  • KyeKye Posts: 2,200
    edited 2013-03-11 10:59
    So I used the technique for something cool I'm working on. I'll wait till I'm mostly done to announce it. With your technique I managed to get 14 quadrature encoder channels and 14 PWM channels in one cog with an update frequency of 64 KHz.
  • pedwardpedward Posts: 1,642
    edited 2013-03-11 12:42
    That's impressive, my application is necessarily limited by pin count. I also have a neat trick for tracking multiple encoders using an ISR. Surprisingly, the generated machine code is rather efficient, perhaps due to trying to make it that way.
  • KyeKye Posts: 2,200
    edited 2013-03-11 13:19
    I plan to make this public domain anyway in the future... so I'll share it... can you guess what its' for? ;)

    Used up pretty much all of the cogs's power for this. Only 4 longs left of space and 8 clock cycles wasted every loop at 64 KHz...

    NOTE: The posted driver is completely untested... the code has never been run. I'm only posting it as an example.
  • pedwardpedward Posts: 1,642
    edited 2013-03-11 14:07
    Once I've got the encoder feedback working in my application, I'm going to work on a digital current mode servo controller.

    The application will be a classic double PID loop, the inner controller will be a current mode PID controller for a DC brushed motor control, then an outer position control loop. I've been exposed to a number of motor control systems, current mode, velocity mode, and systems that do both of those at multiple levels.

    For a design where you don't have a tachometer, the current mode, with encoder feedback, seems the simplest and most reliable.

    I've already got a quad H-bridge driver I made last year that has 4 push-pull MOSFET channels and drivers. I'll use this to try and PWM control a motor. I'm taking the approach of using dedicated low pin count micros to handle the peripheral control, letting the Propeller handle higher level tasks.

    My goal is to make a couple of peripheral devices that work in conjunction with the Propeller to form a robust CNC motion control platform. Using dedicated micros allows me to increase the peripheral count without increasing the pin usage.

    I want to make a PCB router and a PnP machine.

    For the motor controller IC, I intend to use the standard servo protocol to drive direction and current, and make the micro implement the current PID control.

    I'm using the 68HC908QT4 and QY4 chips for this project.
  • kuronekokuroneko Posts: 3,623
    edited 2013-03-12 00:52
    Kye wrote: »
    With your technique I managed to get 14 quadrature encoder channels and 14 PWM channels in one cog with an update frequency of 64 KHz.
    You can go a bit higher still by removing one hub window per channel update:
    {$010}                  long    0, 0, 1, 1, 0, 0, 1, 1
                            long    1, 1, 0, 0, 1, 1, 0, 0
    
                            ...
    
                            ' Update channel N's position - 48 clocks
      
                            test    inaTemp, pinNMask wc
                            test    inaTemp, encoderNQuadrature wz
    
                            muxc    updateChannelN, #%01000 ' New A
                            muxnz   updateChannelN, #%00100 ' New B
                            muxnz   updateChannelN, #%00001 ' Old B (delayed)
    
    updateChannelN          add     encoderNPosition, 0-0   ' update
    
                            rdlong  encoderNQuadrature, encoderNQuadraturePtr wz
    
                            muxc    updateChannelN, #%00010 ' Old A
                            muxz    updateChannelN, #%10000 ' regular/quadrature
    
                            wrlong  encoderNPosition, encoderNPositionPtr
    
  • KyeKye Posts: 2,200
    edited 2013-03-12 08:34
    Ah, I see... you switching between two different tables!

    Man that's clever!

    Okay, I'll integrate this right now :)

    Thanks,
  • KyeKye Posts: 2,200
    edited 2013-03-12 08:38
    Looks like I can hit the next divisor at 75 KHz with that change.
  • KyeKye Posts: 2,200
    edited 2013-03-12 11:42
    Any idea to do this better?
                            cmp     pwm0Cycle,              #0 wz
                            muxnz   diraTemp,               pin0Mask
    
                            cmpsub  pwm0Cycle,              #1 wc
                            cmp     pwm0Cycle,              #0 wz
                        
    if_z                    rdlong  pwm0Cycle,              pwm0CyclePtr
    
    
    if_c                    cmpsub  pwm0Onoff,              #1 wc
                            muxc    outaTemp,               pin0Mask
                        
    if_z                    rdlong  pwm0Onoff,              pwm0OnoffPtr
    
    
    

    I want to do that in 6 instructions... but, the CMPSUB instruction does not set Z=1 if pwm0Cycle=0, only when pwm0Cycle=1. This means that the code will lock up if pwm0Cycle gets set to 0 if I am not doing that compare.
  • pedwardpedward Posts: 1,642
    edited 2013-03-12 12:27
    Since CMPSUB sets wc if a sub is possible, it will always be set if you are counting down, and it will be unset when you are 0, so, you don't even need the zero flag, just whether a reduction was possible:
    Kye wrote: »
    Any idea to do this better?
                            cmpsub  pwm0Cycle,              #1 wc
                            muxc    diraTemp,               pin0Mask
                        
    if_nc                    rdlong  pwm0Cycle,              pwm0CyclePtr
    
    if_c                    cmpsub  pwm0Onoff,              #1 wc
                              muxc    outaTemp,               pin0Mask
                        
    if_nc                    rdlong  pwm0Onoff,              pwm0OnoffPtr
    
    
    

    I want to do that in 6 instructions... but, the CMPSUB instruction does not set Z=1 if pwm0Cycle=0, only when pwm0Cycle=1. This means that the code will lock up if pwm0Cycle gets set to 0 if I am not doing that compare.
  • kuronekokuroneko Posts: 3,623
    edited 2013-03-12 16:52
    pedward wrote: »
    Since CMPSUB sets wc if a sub is possible, it will always be set if you are counting down, and it will be unset when you are 0, so, you don't even need the zero flag, just whether a reduction was possible:
    These two fragments are not equivalent. The first one misses the rdlong for pwm0Cycle == 1.
    cmpsub  pwm0Cycle,              #1 wc
                            [COLOR="#D3D3D3"]muxc    diraTemp,               pin0Mask[/COLOR]
                        
    if_nc                   rdlong  pwm0Cycle,              pwm0CyclePtr
    
    cmpsub  pwm0Cycle,              #1 wc
                            cmp     pwm0Cycle,              #0 wz
                        
    if_z                    rdlong  pwm0Cycle,              pwm0CyclePtr
    
    This may be acceptable (1 cycle delay) but then again it may not.
  • KyeKye Posts: 2,200
    edited 2013-03-12 17:45
    We'll its even worse because the onOff time variable does not update properly. So, it is quite possible to pass a very big value to the cycle time variable and have the channel output incorrectly for a long time. I have to use the Z flag to get the onOff time updated.

    I guess I could pass a dira variable to the driver. But, that means more overhead for the code that will control this driver. My goal is to keep the upper interface as simple as possible.

    I think I'll leave the speed at 64 KHz for now. Even though I can now hit 75 KHz it does not cleanly divide by 256 (I'm not worried about jitter by this because the master 75 KHz frequency will not have jitter... but, I want a nice clean number to report instead).

    The cog will consume less power I guess now ;)

    ---

    If I could get that down to 6 instructions from 8 then I could hit 80 KHz.
  • pedwardpedward Posts: 1,642
    edited 2013-03-12 18:29
    I would reorder it as such:
    cmpsub pwm0Cycle, #1 wc
    if_nc rdlong pwm0Cycle, pwm0Cycleptr
    if_nc muxc diraTemp, pin0Mask
    if_nc nop
    if_nc rdlong pwm0Onoff, pwm0Onoffptr
    if_c cmpsub pwm0Onoff, #1 wc
    muxc outaTemp, pin0Mask
    

    I notice that you test the 'quadrature' bit for every iteration, this is unnecessary because you aren't going to change encoder types on the fly. Change that RDLONG into a TEST.
  • KyeKye Posts: 2,200
    edited 2013-03-12 18:36
    I see, but, that will miss hub windows what you have above... causing it to be slower.
  • pedwardpedward Posts: 1,642
    edited 2013-03-12 18:37
    Kye wrote: »
    We'll its even worse because the onOff time variable does not update properly. So, it is quite possible to pass a very big value to the cycle time variable and have the channel output incorrectly for a long time. I have to use the Z flag to get the onOff time updated.

    I guess I could pass a dira variable to the driver. But, that means more overhead for the code that will control this driver. My goal is to keep the upper interface as simple as possible.

    I think I'll leave the speed at 64 KHz for now. Even though I can now hit 75 KHz it does not cleanly divide by 256 (I'm not worried about jitter by this because the master 75 KHz frequency will not have jitter... but, I want a nice clean number to report instead).

    The cog will consume less power I guess now ;)

    ---

    If I could get that down to 6 instructions from 8 then I could hit 80 KHz.

    I thought centralizing DIRA would be a good idea too, until I considered the actual run. If you polled the DIRA value every cycle, that's ~64,000 HUBops, whereas the distributed DIRA checking is done only when the counter expires, which should be many less HUBops per second.

    My new code should reduce your instruction count, and if you change the RDLONG to a TEST, it will speed it up more.

    Normally it's 3 instructions through, 7 when the counter reloads.
  • pedwardpedward Posts: 1,642
    edited 2013-03-12 18:38
    Kye wrote: »
    I see, but, that will miss hub windows what you have above... causing it to be slower.

    You're only hitting the HUB window on counter reload anyway, so it's not deterministic.
  • KyeKye Posts: 2,200
    edited 2013-03-12 18:40
    About changing encoder types on the fly... this driver is part of an operating system that will load before the users code. I can't assume what they want to do on turn on. Given I'm trying to run at a fixed cycle time I need to be able to handle the worse case conditions.

    The waitcnt makes it deterministic.
  • kuronekokuroneko Posts: 3,623
    edited 2013-03-12 19:00
    Not sure if this is doable and fits into you concept but what if you join pwmNOnoff and pwmNCycle as two words in a long?
    ' Update channel 0's pwm - 32 clocks
    
                            cmpsub  pwm0Cycle,              #1 wc,wz
                            muxc    diraTemp,               pin0Mask
    
    if_z_or_nc              rdlong  pwm0Cycle,              pwm0CyclePtr
    if_z_or_nc              mov     pwm0OnOff,              pwm0Cycle
    if_z_or_nc              and     pwm0Cycle,              h0000FFFF
    
    if_c                    cmpsub  pwm0Onoff,              h00010000 wc
                            muxc    outaTemp,               pin0Mask
    
    Cycle stays in the low word and needs to be masked (for cmpsub reg, #1 w? to work), Onoff can remain in the high word and has |< 16 subtracted instead (low word ignored).

    Just noticed that the update for Onoff is slightly re-ordered but maybe it's still workable.
  • pedwardpedward Posts: 1,642
    edited 2013-03-12 19:07
    A quick calc shows the code is good for 1920RPM with 2000 count encoders.
  • pedwardpedward Posts: 1,642
    edited 2013-03-12 19:17
    The tactic I took with processing encoders was to process them in parallel instead of one at a time.

    Wait for pinmask to not equal last read value
    read encoder pins into "pins"
    copy pins to "pinstmp"
    repeat 14 times
    process lowest 2 bits of pins
    update pinCNTx
    shift pins right by 2
    repeat 14 times
    copy pinCNTx block to HUB
    copy "pinstmp" into "oldpins"

    You could do all the same, just remove the WAITPNE and process the mask by shift until done, interleav the WRLONG calls however you want to not waste HUB clocks.
  • KyeKye Posts: 2,200
    edited 2013-03-12 19:38
    @kuroneko - Yeah, I guess I can do that... I don't think this driver is needed for really low frequency PWM outputs which is what greater than 16-bit resolution would allow you to do. Not really sure what a low frequency PWM output would be good for either. Thanks!

    @pedward - I have to save cogs so I need to double up the processing here with the PWM. My goal is to have really clean output and input so I have to use the waitcnt to make sure the output and direction registers change their state always at the same time. I don't want any jitter.

    Thanks,
  • KyeKye Posts: 2,200
    edited 2013-03-16 12:14
    Okay, here's the result!

    14 regular/quadrature encoder channels and 15 PWM channels in one cog with a sample resolution of 80 KHz (assuming the clock frequency is 96 MHz).
Sign In or Register to comment.