Skip to content

easystepper: add non blocking movement and dynamic speed - #894

Open
zombieleet wants to merge 1 commit into
tinygo-org:devfrom
zombieleet:easystepper-async
Open

easystepper: add non blocking movement and dynamic speed#894
zombieleet wants to merge 1 commit into
tinygo-org:devfrom
zombieleet:easystepper-async

Conversation

@zombieleet

@zombieleet zombieleet commented Sep 12, 2026

Copy link
Copy Markdown

The problem

A stepper motor is slow. A 28BYJ-48 needs 2048 steps for one turn. At 10 rpm
that turn takes 6 seconds.

Move does not return until the motor stops. The program can do nothing in
those 6 seconds.

motor.Move(2048) // the program stops here for 6 seconds
readRemote()     // this runs 6 seconds too late

There is a second limit. New calculates the delay between steps one time and
keeps only that delay. The speed cannot change after that.

stepDelay: time.Second * 60 / time.Duration(config.StepCount * config.RPM)

Why this is a problem

A stepper motor is common in a robot or in a machine that positions a part.
These programs must do other work while a motor turns.

  • A robot with wheels must stop when the operator releases the control. With
    Move it stops only after the movement is complete.
  • A limit switch or a distance sensor must stop the motor immediately.
  • DualDevice turns two motors together at one speed. A robot with one motor
    on each wheel turns a corner with a different speed on each wheel.
  • A heavy load needs a slow start and a slow stop. The speed must change while
    the motor turns.

A goroutine for each motor is not a good solution here. The caller loses
control of the sequence, and each goroutine needs its own stack.

The solution

Update does one step if a step is due. The caller keeps the loop.

motor.MoveAsync(2048)

for motor.IsMoving() {
	motor.Update()
	readRemote() // this runs 1.4 million times while the motor turns
}
call what it does
SetRPM, SetRPMs change speed, also while a motor turns
MoveAsync start a movement of n steps and return at once
Start turn until Stop
Update do one step if a step is due
Stop, IsMoving control and read the movement

Move and Off keep their behaviour. Existing programs do not change. The new
code costs nothing when a program does not use it. The examples/easystepper
binary for microbit is 4344 bytes with this change and 4400 bytes without it.

Three problems found in the tests

Move was one step short. The first pass of the loop applied the step that
the motor was already in.

steps += int32(d.stepNumber)
d.stepMotor(d.stepNumber)
for s = int32(d.stepNumber); s < steps; s++ {
	time.Sleep(d.stepDelay)
	d.moveDirectionSteps(direction, s) // the first pass repeats the same step
}

The value of stepNumber shows the result in 4 step mode.

Move(1)   before: start 0, end 0. No movement, but it waits one delay.
Move(4)   before: start 0, end 3. Three steps.
Move(n)   after:  n steps.

Move and DualDevice.Move now do all n steps. They use the same step code as
MoveAsync. The blocking API and the non blocking API give the same coil
sequence.

Update did a burst of steps after a slow loop. The time of the next step
came from the last time plus the delay. This prevents drift. It is wrong when
the caller is late. The time of the next step is then in the past.

A motor cannot do steps faster than the delay. It loses its position if it must
catch up. With a delay of 4ms and a loop that stopped for 100ms, the motor did
25 steps with no wait. Update now does one step and starts the schedule again.

The dual calculation needs 64 bits. Two step counts near 65535 overflow a
uint32. MoveAsync(100000, 100000) gave the second motor 42949 steps. It now
gives 100000 steps.

Test on hardware

ESP32 with a 28BYJ-48 motor and a ULN2003 board. The LED shows the result. Its
rhythm does not change when the speed of the motor changes.

motor.SetRPM(12)
led.High()
start := time.Now()
motor.Move(turn / 2) // the LED cannot blink in here
fmt.Printf("   half turn took %v, LED was stuck on the whole time\n",
	time.Since(start).Round(time.Millisecond))

motor.SetRPM(3)
motor.MoveAsync(turn)

rpm := uint(3)
nextBlink := time.Now()
nextSpeed := time.Now().Add(time.Second)

for motor.IsMoving() {
	motor.Update()
	loops++

	if time.Now().After(nextBlink) {
		led.Set(!led.Get())
		blinks++
		nextBlink = time.Now().Add(250 * time.Millisecond)
	}

	if rpm < 15 && time.Now().After(nextSpeed) {
		rpm += 3
		motor.SetRPM(rpm)
		fmt.Printf("   %v  rpm is now %2d\n",
			time.Since(start).Round(time.Millisecond), rpm)
		nextSpeed = time.Now().Add(time.Second)
	}
}

Output:

1. Move blocks. Watch the LED stop.
   half turn took 2.507s, LED was stuck on the whole time
2. MoveAsync. LED blinks while the motor speeds up.
   1s  rpm is now  6
   2s  rpm is now  9
   3s  rpm is now 12
   4s  rpm is now 15
   full turn took 6s
   LED blinked 25 times and the loop ran 1470956 times

The times agree with the motor. A half turn is 1024 steps. The delay at 12 rpm
is 60 / (2048 x 12), which is 2.441ms, so the time is 2.500s.

The ramp does 102, 205, 307 and 410 steps in the first four seconds. That is
1024 steps. The last 1024 steps at 15 rpm take 2.0s. The total is 6.0s.

examples/easystepper/async/main.go is the same test without the LED.

Move does not return until the motor stops. A program cannot read a sensor
or a remote control while a motor turns. The speed is also fixed at New.

This adds an API that leaves the loop with the caller.

  SetRPM, SetRPMs  change speed, also while a motor turns
  MoveAsync        start a movement of n steps and return at once
  Start            turn until Stop
  Update           do one step if a step is due
  Stop, IsMoving   control and read the movement

Move and Off keep their behaviour, so existing programs do not change.

Move had an error of one step. The first pass of the loop applied the step
the motor was already in, so Move(n) moved n-1 steps and Move(1) did not
move at all. Move and DualDevice.Move now do all n steps and share their
step logic with MoveAsync, so both give the same coil sequence.

Update must not do a burst of steps when the caller is late, because a
motor loses its position if it must catch up. It does one step and starts
the schedule again.

The dual proportional calculation needs 64 bits. Two step counts near
65535 overflow a uint32 and the slower motor then gets too few steps.

Tested on an ESP32 with a 28BYJ-48 motor and a ULN2003 board.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant