Using onboard Serial1 while driving steppers results in bad step timing

Hi folks!

I have a arduino uno rev2 wifi board (with 2 UARTS) and Serail1 is connected to a rapsberry Pi. The arduino is also driving two nema 17 steppers for a high precision rover robot. Serial1 is used to transmit odometery (every time a step happens).

I’m using something like this every step:

if (Serial1.availableForWrite()) {
        
        Serial1.write((uint8_t*)&odom_struct_, sizeof(struct odom_struct));  
        odom_struct_.lsteps = 0;
        odom_struct_.rsteps = 0;
      }

Steppers are controller by using delayMicroseconds. Concretely this:

void step(long mode1, long mode2) {
        int dir_state1 = curr_dir1;
        int dir_state2 = curr_dir2;

        unsigned long now = micros();
        if((now - last_step_) < step_time_us_) {
          delayMicroseconds(step_time_us_ - (now - last_step_));
        }

        if(mode1 < 0) {
          dir_state1 = 0;
          
        } else if (mode1 > 0) {
          dir_state1 = 1;
        }

        if(mode2 < 0) {
          dir_state2 = 0;
        } else if (mode2 > 0) {
          dir_state2 = 1;
        }

        
        set_direction(dir_state1, dir_state2);
        
        if(mode1 != 0) {
          digitalWrite(step1_, HIGH);
          odom_struct_.lsteps += copysign(1, mode1);
        }

        if(mode2 != 0) {
          digitalWrite(step2_, HIGH);
          odom_struct_.rsteps += copysign(1, mode2);
        }

        delayMicroseconds(20);
      
        digitalWrite(step1_, LOW);
        digitalWrite(step2_, LOW);
        last_step_ = micros();
        
        
    }

Now the issue which I’m having trouble debugging due to the Heisenberg principle (trying to output to serial to debug exacerbates the problem!) is that whenever I write the serial the stepper motor timing is severely affected. I haven’t been able to measure this yet but it is significant enough to make it go a bit zig zag. RPMs drop considerably as well. Without the Serial1.write everything works peachy!

Seems like Serial interrupts might be causing the issue - but I’m usually running the motors at 200rpm, which gives me 1500us in the delay. odom_struct is a 8-byte buffer.

What am I doing wrong and what are some good options to debug this?

Yep, looks as if everything is wrong. Using delay instructions, printing multiple characters in bursts.

For the performance you seek, you will need to write code that interleaves the step timing and printing characters, rapidly alternating between determining whether the necessary time has elapsed for the next step, and whether the UART is ready for another character.

I have some difficulty understanding what "every time a step happens" means; if steps are fast, then you would not expect to have sufficient time to print in any case.

Snippets do not help either. :roll_eyes:

Step happens every 1500us. Serial.write is interrupt driven right? So how would you interleave that not knowing when the interrupt might happen?

The uno wifi rev 2 has a hardware uart on serial1.

Or maybe it'll help to use the non interrupt driven version in this case?

What would be your approach without using a delay? Use a real timer? Every single stepper library I've seen actually used delay so thought it would be okay.

I was sharing snippets since my code is terribly messy and didn't wanna put you through that.

Arduino delay() and delayMicroseconds() BLOCK EXECUTION of the rest of your sketch while they wait. NOTHING ELSE RUNS THEN.

1500 us = 24000 cpu cycles that other routines like printing could be done if those cycles are not wasted.

There is a way to not block and get those cycles that uses a fraction of those to work.
I like Nick Gammon’s (I am not him!) tutorial on the subject, it is simple, practical and complete.
http://gammon.com.au/blink

This is Nick’s how to read serial without blocking tutorial. It has a State Machine lesson that goes farther than just reading serial. State Machine code lets you break a task into steps and run only the wanted step as the machine function is run over and over in void loop().
Gammon Forum : Electronics : Microprocessors : How to process incoming serial data without blocking ← techniques howto

It is hard to overstate how powerful a state machine can be or how simple one can be,.this technique is great for simplifying real world code.

Notice Arduino has

void setup() {
// here you put what runs once at the start
}

void loop() {
// here you put what runs over and over
}

Between non-blocking code and state machines you can automate smoothly up to pretty high speed depending of course on how much else your sketch does.

Thanks! The documentation states that delayMicroseconds does not disable interrupts - I am assuming that my Serial1.write returns instantly and the actual transfer is interrupt based from there on, hopefully occurring mostly in the delayMicroseconds. The state machine might work if the Serial1.write was a blocking call - but its not.

frk1206:
Step happens every 1500us. Serial.write is interrupt driven right? So how would you interleave that not knowing when the interrupt might happen?

Doesn't make any difference. The interrupt will cause the same - random - timing error no matter how you code it, whether delayMicroseconds is happening or the state machine is running.

frk1206:
What would be your approach without using a delay? Use a real timer? Every single stepper library I've seen actually used delay so thought it would be okay.

You just read micros() on each loop and act when it advances by the desired amount. You already have the timer provided for you, no need to complicate matters further.

Stepper libraries are of course for people who want to move steppers and do nothing else of significance at the same time. Same applies for serial libraries/ functions.

frk1206:
Thanks! The documentation states that delayMicroseconds does not disable interrupts

Neither delayMicroseconds or delay() would or could disable interrupts since delay() depends on them. delayMicroseconds simply spins a loop waiting for micros() to advance a given amount - so you might as well do that for yourself and do other things while you are at it. :grinning:

delay() does the same with millis().

Robin2's simple stepper code shows a non-blocking way to control stepper motors.

Non-blocking timing tutorials:
Several things at a time.
Beginner's guide to millis().
Blink without delay().

frk1206:
I am assuming that my Serial1.write returns instantly and the actual transfer is interrupt based from there on, hopefully occurring mostly in the delayMicroseconds.

No. There are no delays written into the serial code. Serial.write() puts a byte into the serial output buffer and Serial puts the next outgoing byte into the UART register then starts it off.

At 115200 baud a byte is sent about every 1388 cycles. If your sketch tries to overfill the output buffer your write or print will have to wait until there's room.

The state machine might work if the Serial1.write was a blocking call - but its not.

Why do you believe that the state machine needs a blocking call to work? Just the opposite is true.

State machines take working with to realize what they can do. State machines are a fantastic coding technique.

frk1206:
Thanks! The documentation states that delayMicroseconds does not disable interrupts - I am assuming that my Serial1.write returns instantly and the actual transfer is interrupt based from there on, hopefully occurring mostly in the delayMicroseconds. The state machine might work if the Serial1.write was a blocking call - but its not.

Yeah, interrupts can run while delay(), etc, hold up execution of the sketch. Thing is that interrupt code needs to be short and usually can't do everything so it sets a flag and maybe stores the time (micros() or millis()) so that the sketch can process that info... if the sketch is blocked then that processing will have to wait and the cycles it waits are lost.

To an Arduino, 1500 usecs is like 15 minutes to you.

So the correct approach in my case is to disable interrupts where timing is important and only enable them in the delayMicroseconds so that Serial write can write itself out. Modified this code works:

        int delay_ms = 0;
        unsigned long now = micros();
        if((now - last_step_) < step_time_us_) {
          interrupts(); 
          delay_ms = step_time_us_ - (now - last_step_);
          delayMicroseconds(delay_ms);
          noInterrupts();
        }

Might be helpful for you guys / others. There’s nothing wrong with using delays and you can write to Serial simultaneously while doing so if you simply control when the interrupts happen. Luckily with a microcontroller this is possible :slight_smile:

You don’t need to disable interrupts for the lessons at the addresses I gave.

That means being able to do way more than you ever can when using blocking code.

Maybe I shouldn’t say “wrong to use delays” when the first word should be “noob”.