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) |