Checking the accuracy of a motor encoder

I always wondered how fancy equipment could check for lost steps on a stepper motor and I think this is one way to do it!

This project uses a powermax II stepper motor fitted with a HEDL-5605 A06 encoder. The motor driver is a DQ542MA. Motor power supply 5A, 24V. The motor is controlled by an Arduino Uno running GRBL (0.83c). The encoder pulses are counted using a 630-HCTL-2032-SC quadrature decoder. The 16 bits of the quad decoder counter are read using an ‘8051 and displayed on a set of 7-segment displays.

I think this is an interesting project as its brings together many different elements:

  • Stepper motor
  • Stepper motor driver
  • Motor encoder
  • Quadrature decoder
  • Arduino loaded with GRBL2Arduino
  • GRBL/g-code
  • Python script to send g-code to the Arduino
  • ‘8051 microcontroller (C8051F410 Silicon Labs)
  • 7-segment displays to display quadrature decoder counter value
  • ‘8051 assembly code
  • MCP23017 IO expander Chips
  • I2C communication
; This code is used to read 16 bits of the counter from a 630-HCTL-2032-SC quadrature decoder that counts pulses from a
; HEDL-5605 A06 motor encoder. The 16 bits are then sent over I2C to MCP23017 I/O expander chips to view on 4x 7-segment
; displays
;
; I used LAoE bitflip.a51 as a starting point from Tom C. Hayes, Learning the art of electronics
; https://learningtheartofelectronics.com/program-listings/silabs-code/lab-c1/
$NOSYMBOLS ; keeps listing short..
$INCLUDE (C:\MICRO\8051\RAISON\INC\c8051f410.inc)
SCL EQU P2.0 ; clock
SDA EQU P2.1 ; data
XY EQU P0.2 ; X*/Y selects channel
ORG 0 ; tells assembler the address at which to place this code
SJMP STARTUP ; here code begins--with just a jump to start of
; real program. ALL our programs will start thus
ORG 80h ; ...and here the program starts
STARTUP:
ACALL USUAL_SETUP
ACALL IO_SETUP
SETB SCL ; initialize with SCL, SDA high
SETB SDA
INIT_EXPANDER: ; this code initializes the MCP23017 chips for output
MOV R1, #00h ; R1 holds value
MOV R2, #00h ; R2 holds the address (DIRA)
MOV R3, #40h
ACALL EXPANDER_WRITE
LCALL DELAY
LCALL DELAY
LCALL DELAY
MOV R1, #00h ; R1 holds value
MOV R2, #12h ; R2 holds the address (GPIOA)
MOV R3, #40h
ACALL EXPANDER_WRITE ; Call the write function
LCALL DELAY
LCALL DELAY
LCALL DELAY
MOV R1, #00h ; R1 holds value
MOV R2, #01h ; R2 holds the address (DIRB)
MOV R3, #40h
ACALL EXPANDER_WRITE
LCALL DELAY
LCALL DELAY
LCALL DELAY
MOV R1, #00h ; R1 holds value
MOV R2, #13h ; R2 holds the address (GPIOB)
MOV R3, #40h
ACALL EXPANDER_WRITE ; Call the write function
LCALL DELAY
LCALL DELAY
LCALL DELAY
MOV R1, #00h ; R1 holds value
MOV R2, #00h ; R2 holds the address (DIRA)
MOV R3, #42h
ACALL EXPANDER_WRITE
LCALL DELAY
LCALL DELAY
LCALL DELAY
MOV R1, #00h ; R1 holds value
MOV R2, #12h ; R2 holds the address (GPIOA)
MOV R3, #42h
ACALL EXPANDER_WRITE ; Call the write function
LCALL DELAY
LCALL DELAY
LCALL DELAY
MOV R1, #00h ; R1 holds value
MOV R2, #01h ; R2 holds the address (DIRB)
MOV R3, #42h
ACALL EXPANDER_WRITE
LCALL DELAY
LCALL DELAY
LCALL DELAY
MOV R1, #00h ; R1 holds value
MOV R2, #13h ; R2 holds the address (GPIOB)
MOV R3, #42h
ACALL EXPANDER_WRITE ; Call the write function
LCALL DELAY
LCALL DELAY
LCALL DELAY
CLR A
MOV R3, #00h
MOV R1, #00h
LCALL GETCODE
MOV R1, ACC ; R1 holds value
MOV R2, #12h ; R2 holds the address (DIRA)
MOV R3, #40h
ACALL EXPANDER_WRITE
LCALL DELAY
LCALL DELAY
LCALL DELAY
MOV R1, #00h
LCALL GETCODE
MOV R1, ACC ; R1 holds value
MOV R2, #13h ; R2 holds the address (DIRA)
MOV R3, #40h
ACALL EXPANDER_WRITE
LCALL DELAY
LCALL DELAY
LCALL DELAY
MOV R1, #00h
LCALL GETCODE
MOV R1, ACC ; R1 holds value
MOV R2, #12h ; R2 holds the address (DIRA)
MOV R3, #42h
ACALL EXPANDER_WRITE
LCALL DELAY
LCALL DELAY
LCALL DELAY
MOV R1, #00h
LCALL GETCODE
MOV R1, ACC ; R1 holds value
MOV R2, #13h ; R2 holds the address (DIRA)
MOV R3, #42h
ACALL EXPANDER_WRITE
LCALL DELAY
LCALL DELAY
LCALL DELAY
INIT_QUAD_DEC: ;this code initializes the quad decoder
//SET FIRST READ MSB
CLR P0.0;
SETB P0.1;
//DISABLE READ LATCH
SETB P0.4;
MAIN_LOOP:
;The main loop is setting axis to X (0) and reading the quad decoder counter
CLR XY
LCALL READ_VAL
LCALL LONG_DELAY
CLR XY
LCALL READ_VAL
LCALL LONG_DELAY
SJMP MAIN_LOOP
READ_VAL: ; 0 1; 1 1; 0 0 ; 1 0
;This is the main code to read the decoder counter
;The counter is 32 bits.
;All 32 bits are read, but only 16 are sent to the 7-seg displays
//OE LOW
CLR P0.4
//SET FOR MSB
CLR P0.0
SETB P0.1
ACALL DELAY;
//MOV R0, P1
SETB P0.4
//SET FOR 3rd
CLR P0.4
SETB P0.0
SETB P0.1
ACALL DELAY;
//MOV R1, P1
SETB P0.4
//SET FOR 2nd
CLR P0.4
CLR P0.0
CLR P0.1
ACALL DELAY;
//MOV R2, P1
;--------
MOV ACC, P1
ANL A, #0F0h
SWAP A
MOV R1, ACC
LCALL GETCODE
MOV R1, ACC ; R1 holds value
MOV R2, #12h ; R2 holds the address (DIRA)
MOV R3, #40h
ACALL EXPANDER_WRITE
LCALL DELAY
LCALL DELAY
LCALL DELAY
;--------
;--------
MOV ACC, P1
ANL A, #0Fh
MOV R1, ACC
LCALL GETCODE
MOV R1, ACC ; R1 holds value
MOV R2, #13h ; R2 holds the address (DIRA)
MOV R3, #40h
ACALL EXPANDER_WRITE
LCALL DELAY
LCALL DELAY
LCALL DELAY
;--------
SETB P0.4
//SET FOR LSB
CLR P0.4
SETB P0.0
CLR P0.1
ACALL DELAY;
//MOV R3, P1
;--------
MOV ACC, P1
ANL A, #0F0h
SWAP A
MOV R1, ACC
LCALL GETCODE
MOV R1, ACC ; R1 holds value
MOV R2, #12h ; R2 holds the address (DIRA)
MOV R3, #42h
ACALL EXPANDER_WRITE
LCALL DELAY
LCALL DELAY
LCALL DELAY
;--------
;--------
MOV ACC, P1
ANL A, #0Fh
MOV R1, ACC
LCALL GETCODE
MOV R1, ACC ; R1 holds value
MOV R2, #13h ; R2 holds the address (DIRA)
MOV R3, #42h
ACALL EXPANDER_WRITE
LCALL DELAY
LCALL DELAY
LCALL DELAY
;--------
//OE HIGH
SETB P0.4
RET
; Expander WRITE AND READ FUNCTIONS
EXPANDER_WRITE:
ACALL I2C_START
MOV A, R3
ACALL I2C_SEND_BYTE
MOV A, R2
ACALL I2C_SEND_BYTE
MOV A, R1
ACALL I2C_SEND_BYTE
LCALL I2C_STOP
LCALL DELAY
RET
I2C_START: ; start condition
CLR SDA
CLR SCL
RET
I2C_STOP: ; stop condition
CLR SDA
CLR SCL
SETB SCL
SETB SDA
RET
;Send Byte assumess accumulator loaded with byte you want to send.
;Basically, just reads bit 7 and pulses SDA appropriately, along with a clock pulse.
;Then, all bits are shifted and the process is repeated until all 8 bits are sent.
;After the 8 bits, an acknowledgement bit is sent.
I2C_SEND_BYTE:
MOV R0, #08
I2C_SEND_LOOP:
JB ACC.7, GO_HIGH
GO_LOW:
ACALL I2C_LOW
SJMP CONTINUE
GO_HIGH:
ACALL I2C_HIGH
SJMP CONTINUE
CONTINUE:
RL A
DJNZ R0, I2C_SEND_LOOP
I2C_SEND_BYTE_DONE:
ACALL I2C_HIGH //ACK
RET
I2C_HIGH:
SETB SDA ; data high
SETB SCL ; Clock hi edge
SETB SDA ;stay high
CLR SCL ; clear clock
RET
I2C_LOW:
CLR SDA ; data low
SETB SCL ; clock edge hi
CLR SDA ; stay low
CLR SCL ; clear clock
RET
; I2C READ BYTE
; Assumes slave is ready to send data.
; Basically, sends clock pulses and polls SDA and sets/clears ACC.0 depending on value.
; Then, the ACC is left shifted until the byte is read.
; Read byte is left in accumulator upon return from subroutine
I2C_READ_BYTE:
MOV R0, #08
MOV ACC, #00h
I2C_READ_LOOP:
RL A
SETB SCL; clock edge hi
JB SDA, READ_HIGH
READ_LOW:
CLR ACC.0
SJMP CONTINUE_READ
READ_HIGH:
SETB ACC.0
SJMP CONTINUE_READ
CONTINUE_READ:
CLR SCL; clock low
DJNZ R0, I2C_READ_LOOP
I2C_SEND_BYTE_DONE_READ:
ACALL I2C_HIGH //ACK
RET
USUAL_SETUP: ; Disable the WDT.
anl PCA0MD, #NOT(040h) ; Clear Watchdog Enable bit
; Enable the Port I/O Crossbar
mov XBR1, #40h ; Enable Crossbar
ret
IO_SETUP: orl P0MDOUT, #01h ; enable P0.0 as push-pull output. Whoops forgot this was here. Looks like everything still worked ok
; though.
RET
DELAY:
MOV R7, #00h
WAIT:
DJNZ R7, WAIT
RET
LONG_DELAY:
MOV R6, #50h
INNER:
MOV R7, #00
LONG_WAIT:
DJNZ R7, LONG_WAIT
DJNZ R6, INNER
RET
GETCODE: MOV A, R1
ADD A, #01
MOVC A, @A+PC ; that we must read beyond
RET
;This data table shows which segment leds to turn on
DB 00111111b ; 3F
DB 00000110b ; 06
DB 01011011b ; 5B
DB 01001111b ; 4F
DB 01100110b
DB 01101101b
DB 01111101b
DB 00000111b
DB 01111111b
DB 01101111b
DB 01110111b
DB 01111100b
DB 00111001b
DB 01011110b
DB 01111001b
DB 01110001b
END

Experimented a bit further, accuracy is much higher by increasing the clock frequency to 100 kHz and steps/rev to 400 (OFF/ON/ON/ON). I realized in the first video, the motor driver DIP switches were set to OFF/OFF/ON/ON = 1600 steps/rev. With these settings, the encoder and quad decoder accurately detect the motor shaft position; observed no missing pulses 0 to 50 in steps of 2.5, and back to x=0 again; then 0 to 50 in steps of 1, and back to x=0 again.

Wrote a python script to send g-code to the Arduino to automate the motor test.

import serial
import time
ser=serial.Serial('COM5', 9600)
counter = 0
ser.write('g0x0\n')
time.sleep(1)
time.sleep(1)
while counter <50:
current = str(int(counter))
print current
a = 'g0x' + current + '\n'
ser.write(a)
print a
time.sleep(2.5)
counter += 2.5
time.sleep(2.5)
while counter >.2:
current = str(int(counter))
print current
a = 'g0x' + current + '\n'
ser.write(a)
print a
time.sleep(2.5)
counter -= 2.5
time.sleep(2.5)
time.sleep(2.5)
ser.write('g0x0\n')
time.sleep(5)
ser.write('g0x0\n')
time.sleep(1)
time.sleep(1)
while counter <50:
current = str(int(counter))
print current
a = 'g0x' + current + '\n'
ser.write(a)
print a
time.sleep(2.5)
counter += 1
time.sleep(2.5)
while counter >.2:
current = str(int(counter))
print current
a = 'g0x' + current + '\n'
ser.write(a)
print a
time.sleep(2.5)
counter -= 1
time.sleep(2.5)
time.sleep(2.5)
ser.write('g0x0\n')
time.sleep(5)
view raw test GRBL.py hosted with ❤ by GitHub