Encoder expander circuit and code

Thread Starter

cmartinez

Joined Jan 17, 2007
8,722
I recently completed a small project that required the use of a two-channel linear incremental encoder.

When I got started I ran into a very peculiar problem. The machine's position had to be monitored at all times, and sometimes the machine moved so fast that the MCU lost track of the encoder's signals, immediately reporting an error. This was because the MCU has to monitor other things too, and not only the encoder. It also monitors an emergency stop button, several special function switches and an error report signal from the servo-driver. And it has to increment or decrement position registers too, among other things. So performing monitoring sweeps while also doing arithmetic operations is very time consuming.

There was nothing wrong with the encoder, except that its pitch was way too small for what I needed. That is, the encoder's original pitch is only 0.005 mm per pulse, while my application required a precision of no more than 0.1 mm. So it turned out that I was using an encoder with 20 times more resolution than I actually needed!

This small project is based on the AT89LP4052 MCU, which uses the 8051 architecture. It's really fast (only one clock cycle per average instruction) and has 15 configurable I/O

Also used is a AM26LS32AC differential line receiver. This is because the encoder's outputs are differential and must be translated to single-ended before interfacing them to the MCU

What the program does is, it monitors the encoder's channels and expands their signals into multiples of its original pitch. The amount of expansion depends on the value assigned to a variable named ENCODER_ADDER, and it is recommended that this is always an odd number, for reasons explained in the attached Excel worksheet, and the assembly code.

The attached zip file contains the HEX code, the .asm file, a DWG drawing of the circuit, and an Excel sheet with the expander's simulation numbers.

I've also included the PDF file showing the contents of the DWG drawing, for those unable to open that kind of file.

The part list is rather short:
1 - AT89LP4062 MCU (Digikey AT89LP4052-20PU-ND, $1.58 U.S.D.)
1 - AM26LS32AC differential line receiver (Digikey 296-1376-5-ND, $0.91 U.S.D.)
2 - 100 nF ceramic bypass capacitors (Digikey 478-1395-1-ND, $0.10 U.S.D.)
1 - 14.7465 Mhz crystal oscillator (Digikey CTX089-ND, $0.66 U.S.D.)
1 - 20-pin ic socket
1 - 16-pin ic socket
5 - 5mm pitch PCB connectors (Digikey ED2714-ND, $0.78 U.S.D)


Capture 04.PNG


Capture 01.PNG

9cfcdc4b-27f4-49e8-94bb-142617a6e8d9.jpg


Capture 03.PNG
Code:
;*****************************************************************************************
;                                  1/29/2017 9:32:56 PM:
;             Copyright 2017 TechnoMech, S.A. de C.V. info@technomech.info
;
;     Encoder shifter program, this program has motion error check.
;     This program takes the input of a 2-channel encoder, and produces an output that
;    has a longer waveform than the original, with the purpose of lowering the resolution,
;    thus delivering a waveform at a slower frequency than the original.
;     The technique being used to accomplish this, is simply waiting for an extra number
;    of events (determined by the ENCODER_ADDER register) before effectively
;    changing the output channel.
;
;*****************************************************************************************

F1             EQU D1H ;address of PSW.1, to be used as a flag

;************************************* I/O definitions ***********************************
OUT_CHAN_A     EQU 94H ;P1.4, output, stretched waveform, channel A
OUT_CHAN_B     EQU 95H ;P1.5, output, stretched waveform, channel B
INP_CHAN_A     EQU 96H ;P1.6, input, encoder's channel A
INP_CHAN_B     EQU 97H ;P1.7, input, encoder's channel B

;******************************* Register name substitutions *****************************
CURR_ENCODER: REG R2
PREV_ENCODER: REG R3

;*********************** address definitions of bit addressable RAM **********************
STATUS0          EQU 20H    ;status byte
ERROR_FLAG         EQU 00H  ;define bit address 00H as variable ERROR_FLAG

;************************* address definitions general purpose RAM ***********************
COUNT_CHAN_A     EQU 30H  ;encoder counter, channel A
COUNT_CHAN_B     EQU 31H  ;encoder counter, channel B
ENCODER_ADDER    EQU 32H  ;additional events needed per encoder input channel to reflect a
                          ;change at their outputs
ENCODER_OVERFLOW EQU 33H  ;this simplifies programming, by assigning it a constant value
                          ;of ENCODER_ADDER+1

;************************** commands and instructions definitions ************************

;********************************** constants definitions ********************************

;-------------------------------------------
FILTER_ENCODER   EQU 11000000B
;-------------------------------------------
POSD             EQU 10000000B
POSC             EQU 11000000B  ;^  increase
POSB             EQU 01000000B
POSA             EQU 00000000B


;*********************************** set I/O's initial conditions **************************************


         ;*********************** Ports I/O definitions ***********************
         ;************* ignore compiler errors for these addresses ************
         ;------------- port 1 ---------------
         ;(P1.0) = not used
         ;(P1.1) = not used
         ;(P1.2) = not used
         ;(P1.3) = not used
         ;(P1.4) = output, stretched waveform, channel A
         ;(P1.5) = output, stretched waveform, channel B
         ;(P1.6) = input, encoder's channel A
         ;(P1.7) = input, encoder's channel B
         MOV     C2H, #11001111B ;ignore compiler error
         MOV     C3H, #00110000B ;ignore compiler error

         ;------------- port 3 ---------------
         ;(P3.0) = not used
         ;(P3.1) = not used
         ;(P3.2) = not used
         ;(P3.3) = not used
         ;(P3.4) = not used
         ;(P3.5) = not used
         ;(P3.6) = unusable in the AT89LP4052
         ;(P3.7) = not used
         MOV     C6H, #11111111B ;ignore compiler error
         MOV     C7H, #00000000B ;ignore compiler error


;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;                                   **** Set initial conditions ****
; The formula to obtain the value of ENCODER_ADDER, by plugging in the desired output resolution R (which
;will always be a multiple of the original encoder's 0.005 mm/pulse resolution) is:
;                                    ENCODER_ADDER = 2*R/0.005 - 1
; For consistency, ENCODER_ADDER must always be an odd number
   MOV ENCODER_ADDER, #39D ; Set the output resolution at 0.100 mm/pulse
                           ; Check the "Econder Pattern 03.xls" file to make sense of why it is being
                           ;done this way
   MOV ENCODER_OVERFLOW, ENCODER_ADDER ;including this variable simplifies programming later on
   INC ENCODER_OVERFLOW                ;ENCODER_OVERFLOW = ENCODER_ADDER + 1
;<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

   MOV COUNT_CHAN_A, #00D  ;The counter for channel A will always start at zero
   MOV A, ENCODER_OVERFLOW
   CLR C                   ;necessary for the next instruction, to avoid carring a 1 into the MSb of A
   RRC A
   MOV COUNT_CHAN_B, A     ;The counter for channel B will always start (ENCODER_ADDER + 1)/2

   SETB OUT_CHAN_A         ;startup conditions of output signals
   CLR  OUT_CHAN_B

   ;----- initialize encoder registers
   MOV A, P1               ;read encoder inputs into A
   ANL A, #FILTER_ENCODER  ;filter only the pertinent values of the encoder
   MOV CURR_ENCODER, A     ;load the actual encoder reading into CURR_ENCODER
   MOV PREV_ENCODER, A     ;load the previous encoder reading into PREV_ENCODER


;********************************************************************************************************
; Monitor the encoder's inputs and update the current position. Reporting its value if any change has
;taken place
;********************************************************************************************************
MONITOR_CIRCUIT:
                                    ;read the left and right signals
         MOV A, P1                  ;of the encoder into A (new values)
         ANL A, #FILTER_ENCODER     ;filter only the pertinent values of the encoder
         MOV CURR_ENCODER, A        ;load the actual encoder reading into CURR_ENCODER

         MOV B, PREV_ENCODER        ;necessary for the next instruction
         CJNE A, B, CHANGE_HAPPENED ;detect if motion or the push of a button has been performed
       JMP MONITOR_CIRCUIT          ;if not, keep looking for change in encoder inputs

  CHANGE_HAPPENED:

   ; Compare the state of CURR_ENCODER (current reading) vs PREV_ENCODER (previous reading) and increment
   ; or decrement the counter accordingly, also monitoring for error detection.
   ;
   ; This is the following valid encoder reading, after filtering:
   ;
   ;------------------------------------------
   ; FILTER_ENCODER EQU 11000000B
   ;------------------------------------------
   ;           POSD EQU 10000000B
   ;           POSC EQU 11000000B  ^  increase
   ;           POSB EQU 01000000B
   ;           POSA EQU 00000000B

      SC00:
        CJNE CURR_ENCODER,  #POSA, SC01
       SC00FWD:
         CJNE PREV_ENCODER, #POSB, SC00REV
         JMP INCREMENT
       SC00REV:
         CJNE PREV_ENCODER, #POSD, SC00ERR
         JMP DECREMENT
       SC00ERR:
        ;CJNE PREV_ENCODER, #POSC, SC01 ;<- not necessary, since this is the only option left
         SETB ERROR_FLAG
        JMP CONT_LOOP

      SC01:
        CJNE CURR_ENCODER,  #POSB, SC11
       SC01FWD:
         CJNE PREV_ENCODER, #POSC, SC01REV
         JMP INCREMENT
       SC01REV:
         CJNE PREV_ENCODER,  #POSA, SC01ERR
         JMP DECREMENT
       SC01ERR:
        ;CJNE PREV_ENCODER, #POSD, SC11
         SETB ERROR_FLAG
        JMP CONT_LOOP

      SC11:
        CJNE CURR_ENCODER,  #POSC, SC10
       SC11FWD:
         CJNE PREV_ENCODER, #POSD, SC11REV
         JMP INCREMENT
       SC11REV:
         CJNE PREV_ENCODER,  #POSB, SC11ERR
         JMP DECREMENT
       SC11ERR:
        ;CJNE PREV_ENCODER, #POSA, SC10
         SETB ERROR_FLAG
        JMP CONT_LOOP

      SC10:
       ;CJNE CURR_ENCODER, #POSD, CONT_TRANS ;<- not necessary, since this is the only option left
       SC10FWD:
         CJNE PREV_ENCODER, #POSA, SC10REV
         JMP INCREMENT
       SC10REV:
         CJNE PREV_ENCODER,  #POSC, SC10ERR
         JMP DECREMENT
       SC10ERR:
        ;CJNE PREV_ENCODER, #POSB, CONT_TRANS
         SETB ERROR_FLAG
        JMP CONT_LOOP

    INCREMENT:
      INC COUNT_CHAN_A  ;both counters are incremented, and then checked for overflow
      INC COUNT_CHAN_B

     ;see if either COUNT_CHAN_A or COUNT_CHAN_B overflowed to ENCODER_OVERFLOW, and act accordingly
     CHECK_OVERFLOW_CHAN_A:
        MOV A, COUNT_CHAN_A
        CJNE A, ENCODER_OVERFLOW, CHECK_OVERFLOW_CHAN_B
        MOV COUNT_CHAN_A, #00H ;reset channel A counter to zero
        CPL OUT_CHAN_A         ;change output signal state
      JMP CONT_LOOP            ;it's not possible for both channels to overflow at the same time
                               ;so skip checking if channel B overflowed too
     CHECK_OVERFLOW_CHAN_B:
        MOV A, COUNT_CHAN_B
        CJNE A, ENCODER_OVERFLOW, CONT_LOOP
        MOV COUNT_CHAN_B, #00H ;reset channel B counter to zero
        CPL OUT_CHAN_B         ;change output signal state
     JMP CONT_LOOP

    DECREMENT:
      DEC COUNT_CHAN_A         ;both counters are decremented, and then checked for underflow
      DEC COUNT_CHAN_B

     ;see if either COUNT_CHAN_A or COUNT_CHAN_B underflowed to 255, and act accordingly
     CHECK_UNDERFLOW_CHAN_A:
        MOV A, COUNT_CHAN_A
        CPL A
       JNZ CHECK_UNDERFLOW_CHAN_B
        MOV COUNT_CHAN_A, ENCODER_ADDER ;set channel A counter to its maximum allowable value
        CPL OUT_CHAN_A         ;change output signal state
       JMP CONT_LOOP           ;it's not possible for both channels to underflow at the same time
                               ;so skip checking if channel B underflowed too
     CHECK_UNDERFLOW_CHAN_B:
        MOV A, COUNT_CHAN_B
        CPL A
       JNZ CONT_LOOP
        MOV COUNT_CHAN_B, ENCODER_ADDER ;set channel B counter to its maximum allowable value
        CPL OUT_CHAN_B         ;change output signal state

    CONT_LOOP:

     MOV PREV_ENCODER, CURR_ENCODER     ;update historic variables

   JMP MONITOR_CIRCUIT


;*** This is the last line of the program, to MAKE SURE that the compiler reads until the last end-of-line of the file
And here's the Hex file of the compiled program:

Code:
:1000000075C2CF75C33075C6FF75C70075322785B9
:1000100032330533753000E533C313F531D294C262
:1000200095E59054C0FAFBE59054C0FA8BF0B5F01A
:10003000020127BA0011BB4003020080BB8003020B
:10004000009ED2000200B9BA4011BBC0030200807A
:10005000BB000302009ED2000200B9BAC011BB80EF
:1000600003020080BB400302009ED2000200B9BB25
:100070000003020080BBC00302009ED2000200B950
:1000800005300531E530B53308753000B294020013
:10009000B9E531B53323753100B2950200B9153099
:1000A0001531E530F47008853230B2940200B9E5BC
:0D00B00031F47005853231B295AB020127A5
:00000001FF
 

Attachments

Last edited:

dendad

Joined Feb 20, 2016
4,635
I don't know 8051 code, but are the encoder pulses generating an interrupt? I is a lot better to use IRQs for critical timing than polling.
Also, if the encoder is 20x the reslution needed, can you gear it down or use another lower res encoder? If you can, the processor will have so much more time to handle stuff so will be less likely to miss pulses.
 

Thread Starter

cmartinez

Joined Jan 17, 2007
8,722
are the encoder pulses generating an interrupt?
No, this program works through polling. That is, the program works through continuous cycles that monitor changes at the inputs, and translates and reports those changes at the outputs. I could've used interrupts of course, but in this case I think the logic is easier to read and follow without interrupts.

can you gear it down or use another lower res encoder?
Yes, of course I could've used a lower res encoder. But that is what I had with me at the moment. The encoder I used was a remnant from an older project, and buying one with the resolution I wanted would've been costly. Besides, I hate waste, so putting this encoder to good use seemed like the proper thing to do.
As for gearing it down, in this case that would've not been possible, since the encoder is of the non-contact magnetic linear type. And even if it would've been of some other type, like optical rotary for instance, it still makes more sense to use this little circuit since it has no moving parts and does not add mechanical complexity, thus making the system far more reliable and robust. And that's not considering that it's also more economical. The 89LP4052 chip itself costs less that $2.00 dlls.

This circuit is completely separate from the main processor, and acts as a conditioning interface between the processor and the encoder. And so the whole point is precisely to lessen the work load on the processor.
 

GopherT

Joined Nov 23, 2012
8,009
I could've used interrupts of course, but in this case I think the logic is easier to read and follow without interrupts.
Polling may be easier but it doesn't work. Also, if you do poll, you can incorporate a "manual interupt" that polls your input with code that is interlaced with the instructions to do the other tasks you want to do.

I would use interupts.
 

Thread Starter

cmartinez

Joined Jan 17, 2007
8,722
Polling may be easier but it doesn't work
Thanks for the suggestion.. but it does work...

On the other hand, I have to admit I'm biased against interrupts... I just hate not knowing what the controller is doing at all times... I know how to use interrupts, I just don't like them.
But it's really just me. I'm certain that far more knowledgeable people than me implemented interrupts into MCU's for very good reasons.

Now that you've brought it up, I've got the itch to start doing some experimenting with interrupts with this application. See if it makes the code smaller and/or easier to follow.
 

GopherT

Joined Nov 23, 2012
8,009
Thanks for the suggestion.. but it does work...

On the other hand, I have to admit I'm biased against interrupts... I just hate not knowing what the controller is doing at all times... I know how to use interrupts, I just don't like them.
But it's really just me. I'm certain that far more knowledgeable people than me implemented interrupts into MCU's for very good reasons.

Now that you've brought it up, I've got the itch to start doing some experimenting with interrupts with this application. See if it makes the code smaller and/or easier to follow.

If you don't like the spontaneous jump to the interupt services routine, you can simply set and check interupt flags. The PIC can have the flag triggered for an interupt-on-change for an input (a persistent flag) that can be checked with code rather than the automatic service routine.
 

Sensacell

Joined Jun 19, 2012
3,768
Have you determined the maximum encoder slew rate and therefore count rate?
You may find that a soft solution is not going to cut it - regardless, even with interrupts, you always have a certain max frequency you can count.
There are hardware chips that can do the nasty for you:

http://www.lsicsi.com/encoders.htm
 
Top