Machine week
We decided to make a kinetic sand table.
- mechanical structure: coreXY
- Permanent magnet. Possible improvement: electromagnet, would allow to change the depth trace.
Day 1
Functional blocks
- Enclosure
- Mechanical structure definition
- Force transmission
- Motors:
- choice
- control
- Homing procedure
- End-switch on both axis
- Trajectory generation
- LEDs
µC
pins:
- 2 per motor (step + dir) or 4 per motor (1 per phase)
- 1 per end-switch
- M0, M1 for micro-stepping option
- 1 for the LED strip
First step: list what's available in our fab lab.
I found:
- 2 Pololu DRV8834 module, to drive our 2 stepper motors
- 1 Xiao RP2040
- 2 stepper motor
Day 2
Goal: define the electronics schematic (at least a first version).
I started by connecting the Xiao, a stepper driver module and a stepper motor to check the connections.
I wanted to be sure of the phase order for the motor.

Test circuit
The driver module has 3 inputs that must be set externally:
- DIR sets the rotation direction (clockwise or counter-clockwise)
- STEP: a rising edge on this pin tells the motor to take a step
- nSLEEP puts the driver in sleep mode. It's active low.
The stepper motor is bipolar and comes with its own cable. On the microcontroller side, it has a 4 pin header.
One simple question is "Does its pin order matches the driver module's one ?".
I wrote a simple test code in microPython to answer this question:
- it sets DIR to 0
- it disables nSLEEP
- it outputs a 50% PWM signal on STEP
# machine week stepper test with wire connections
# GP0 = DIR
# GP1 = STEP
# GP2 = nSLEEP
from machine import Pin, PWM
# set P26 as output and its state to 0 (direction is not important for this test)
DIR = Pin(26, Pin.OUT)
DIR.off()
# set P28 as output and its state to 1 to activate the driver (nSLEEP is active low)
nSLEEP = Pin(28, Pin.OUT)
nSLEEP.on()
# create a PWM object on pin P27, set freq = 5 kHz and duty cycle = 50%
pwm = PWM(27, freq=5000, duty_u16=32768)
while True:
pass
The motor should turn smoothly at constant speed.
If the motor doesn't move, there must be a problem in the signals generation.
If the movement is "jerky", it means that the phase order is not correct.
The test was successful.
Day 3: PIO coding
The goal is to be able to control 2 steppers synchronously, using the RP2040's PIO.
pio-1stepper-1.py
I started with a code to control a single stepper with a PIO's state machine.
It is based on the code I made during the Outputs week (that was designed for another driver).
import rp2
from machine import Pin
@rp2.asm_pio( set_init=(rp2.PIO.OUT_LOW) )
def step():
wrap_target()
pull() # pull a value from the RX-FIFO (fed by the DMA) to OSR
out(isr, 16) # copy the OSR's 16 lsb to ISR (step duration)
out(y, 16) # copy the OSR's 16 msb to y (nb. of steps)
label("step")
set(pins, 1)
mov(x, isr) # initialize the duration counter
set(pins, 0)
label("counting")
jmp(x_dec, "counting")
jmp(y_dec, "step")
wrap()
pioId = 0 # pio module id (0-1)
smId = 0 # state machine id (0-3)
# creates the state machine object and activates it
sm = rp2.StateMachine(4*smId+pioId, step, set_base=Pin(2), freq=10000)
sm.active(1)
# DMA
data = [4, 20]
dataLen = len(data)
byteData = []
for i in range (dataLen):
byteData.append( data[i]%256 )
byteData.append( data[i]//256 )
byteData = bytearray(byteData)
dataReqIdx = (pioId << 3) + smId # DMA request id for our state machine
d = rp2.DMA() # creates the DMA channel
c = d.pack_ctrl(size=2, # Transfer bytes
inc_write=False, # don't increment the write address
treq_sel=dataReqIdx) # transfer is initiated by the state machine
d.config(
read=byteData, # data source is our buffer
write=sm, # data destination is our state machine RX FIFO
count=dataLen//2, # number of data to send
ctrl=c,
trigger=False)
d.active(1)
while d.active():
pass
Results:
data = [4, 20]:
5 pulses (200 µs), period 2.5 ms
data = [4, 10]:
5 pulses (200 µs), period 1.5 ms
data = [4, 0]:
5 pulses (200 µs), period 0.5 ms
SM clock period = 100 µs
=> step duration = (0.5 + X*0.1) ms = (5 + X) clock period
pio-1stepper-2.py
Next, I improve it by using the sideset capability of the PIO's instructions.
I also added some data encoding to ease sending 16-bit values in the DMA channel.
pio-2_steppers-1.py
Time to try controlling two steppers.
The code configure two state machines to execute the same code.
Each of them has its own DMA channel.
It works, but there is a 120 µs delay between the output signals. It can be caused by the execution time of the microPython program or by the clock speed of the state machines: a clock cycle is 100 µs.
pio-2_steppers-2.py
Anyway, I still have to generate the DIR signals. I didn't find an easy way to do it within the STEP generation code.
Hence, I decided to use a third state machine to generate them and to synchronize the STEP signals generation, using the PIO IRQs.
Day 4: PCB
I designed the PCB footprint for pololu module throughole => hack to switch with SMD
engraving OK
soldering OK, tweezer had to be repared
testing: modified version of stepper-test:
one stepper ok, the other nothing
try switching motors, then driver => problem from the pcb
logic analyzer: remove driver, command signals OK
Oscillocope: test power pins: 9V missing, forgot to solder one pin
solder it: both motors turn, but one quicker than the other. I didn't solder the wires that connect M0 and M1 signal.
M0 should be floating. By setting M0 as an input, it works.
Day 5: Assembly ?
Early morning problem
I made the calculations to transform steps in turns, then in distance:
- motor has 200 steps/turn
- By default, pololu module are in 1/4 step mode => 1 turn = 800 steps
I tested it with my motor test code, but the motor didn't turn.
I checked the PCB and fond that the RP2040 was (very) hot.
I looked for short-circuit between all its pins (with a multimeter) and found none.
Then, I try to connect the Xiao alone to my computer, it became hot again.
The problem was with the Xiao. I soldered sockets on a new one and tried my code again.
It worked.
I don't know what caused the Xiao failure.
The test was conclusive: I asked 8000 steps and the motor pulley made 10 turns.
In my current code (pio-class_Steppers-1.py), I hav no way to tell when the movement ends. I have to add that in my state machines.
mechanical debug
Fabio and I did some tests on the coreXY structure
The L-pieces could use some modifications:
- the 3 holes for the wheel screws is too small, we had to drill them up to 5 mm diameter
- a spacer has to be added between the wheel and the piece. it could be included in the L-piece
- The moving bar should be a bit smaller to avoid touching the structure
One of the feet broke. The walls of the pocket for the aluminium beam are too thin.
Elodie was homeworking today, we send her the modifications to make on the 3D models.
Fabio started to design pieces to attach the end-switches to the structure.
I continue writing the Python code.
New code features
The actual code misses several features:
- the main code can start a movement, but has no way to know when it finished
- nSLEEP, M0 and M1 has to be controlled properly
- The end-switches must be implemented.
I'll start with the movement end detection.
End of movement detection
I decided to use PIO's IRQ again.
- The step generation state machines will set an IRQ flag when their current sequence ended
- The main state machine will wait for these iRQ flag to be set before pushing a data in its Rx FIFO
- The main loop can check if there is a new data in the main state machine FIFO to know if the movement has ended.
Don't forget to read the data to empty the FIFO.
My implementation (pio-coreXY-1.py) did not work properly. Th
def isMoving(self):
if self.sm2.rx_fifo() > 0:
self.sm2.get()
print("stopped")
return False
else:
return True
solder 2 switches
add a new function test it, Y always pressed, voltage = 2.74V ?? It appears that on the Xiao, pins P3 and P4 are not in order.
Day 6: XY movements
Next step will be to send commands on motors based on XY coordinates
isMoving don't work again !!
Let's do the math
Our main source for the coreXY is https://corexy.com/theory.html.
We have: $$$ dA = dx + dy dB = dx - dy $$$
where
- dA, dB are the distance of motor A and B
def moveXY(self, dx, dy):
dA = dx + dy
dB = dx - dy
l = sqrt(dx*dx + dy*dy)
T = l / self.nomSpeed
nA = int(dA * self.stepPerMeter)
nB = int(dB * self.stepPerMeter)
if nA != 0:
tA = abs(int(self.freq*T/nA))
else:
tA = 0
if nB !=0:
tB = abs(int(self.freq*T/nB))
else:
tB = 0
print(nA, tA, nB, tB)
self.moveStep(nA, tA, nB, tB)
time.sleep(T)