Using 16-Bit Timer As Hardware Step Pulse Generator

I need to drive a stepper motor with a very specific acceleration/deceleration profile. The way I'd like to do it is to use a 16-bit timer with compare to generate the pulses to the external stepper driver. I'm hoping someone here knows the Arduino timers well enough to tell me if what I'd like to do is possible, before I take the time to study the datasheet to understand the specifics.

Here's what I have in mind:

The acceleration profile is stored in a 225-entry lookup table, with each entry being a single step period, in uSec. The profile provides a step-by-step acceleration ramp from 0 RPM up to the motors maximum RPM. To start motion, the timer would be cleared, then started counting at a rate of 1uSec/count. The compare register would be set to the value in the first table entry, which is the longest delay.

I am assuming the timer can be programmed to generate a rising edge on an output pin when the counter reaches the compare value. This will trigger a step from the stepper driver hardware. I assume I can also generate an interrupt when the compare match occurs. The interrupt handler will then reset the output pin, load the NEXT (shorter) entry from the table, add that value to the previous compare value, and write that value into the compare register. In this way, the active edge of each step pulse is created by hardware (so very low jitter), the counter counts up continuously, and the compare value always stays ahead of the counter (assuming interrupts are handled promptly), except for the brief period between the compare and the associated interrupt. Is that all do-able?

The above repeats until the last entry from the table is loaded into the compare register, at which time the motor is moving at maximum speed, so that last table entry is added to the compare register on each update. The motor therefore continues to move at maximum speed until a stop is commanded.

When a stop is commanded, the compare register is updated using values read from the table in reverse order, decelerating the motor using the acceleration profile in reverse.

Is there any reason this will not work? I am hoping to prototype it using a 328P Timer1, but hope it can eventually run on an ATTiny-1604 TimerB.

Any comments or problems with this approach?

Regards,
Ray L.

Ray, could you use tone() for this? Using a higher-level Arduino function like that might make porting your code between chips easier. As long as tone() is implemented, the code might work unaltered. Whereas accessing timer hardware will be slightly different on every chip, even between AVR chips.

PaulRB:
Ray, could you use tone() for this? Using a higher-level Arduino function like that might make porting your code between chips easier. As long as tone() is implemented, the code might work unaltered. Whereas accessing timer hardware will be slightly different on every chip, even between AVR chips.

I'm not really concerned about the porting, since I don't see any reason I'd ever need to change chips. tone would NOT work as it's a blocking function, and not designed to handle per-pulse delays that change on every pulse. I need a pulse train where the inter-pulse delay is different for EVERY pulse, and those delays are very, very precise. A single incorrect delay will stall the motor.

Regards,
Ray L.

i'm curious, why use a hardware timer and not just track time periods by reading micros() to determine when to step and what the next period is?

gcjr:
i'm curious, why use a hardware timer and not just track time periods by reading micros() to determine when to step and what the next period is?

Because the timing needs to be precise, to the uSec, and there is lots of other stuff that has to continue going on at the same time. Software timing == jitter == reduced performance, and possibility of lost steps or outright motor stalls.

Regards,
Ray L.

so you want a one-shot timer interrupt and the next period needs to be reset in the isr

RayLivingston:
it's a blocking function

Nope.

gcjr:
so you want a one-shot timer interrupt and the next period needs to be reset in the isr

Yes, but the next period needs to start time the instant the previous pulse is issued. Hence allowing the counter to keep running, and updating the compare register in the ISR.

Regards,
Ray L.

PaulRB:
Nope.

OK, then explain how that would work, with a different delay for EVERY pulse, and precise, to the microsecond, timing of all pulses. Last I knew, tone was a blocking function but that was quite a while ago.

Regards,
Ray L.

This compiles but I haven't checked it on a scope. It's for a Mega2560 but with a few changes should be adaptable to an Uno. You just need to choose a 16-bit timer which has an OC brought out to a pin.

If functional, this is a subset of what you want as it doesn't decelerate the stepper but it could be modified to do so.

I also guessed at stepping rates and the step pulse width. The clock prescalers give /8 or /164 (no /16) so I chose the /8. You may need to go to /64 if you want longer periods between steps etc.

#define INIT_STEP_DELAY     10000           //#     uS = N*500nS    5mS initial step delay
#define STEP_PULSE_WIDTH    100             //#     uS = N*500nS    50uS step pulse width
#define NORMAL_STEP_TIME    2000            //#     uS = N*500nS    1mS step period "at speed"
#define ACCEL_NUMSTEPS      20              //#                     size of number of steps in acceleration profile

#define SW_RD_INTERVAL      50ul            //mS    mS = N          switch read interval (mS)

const byte pinMEGA_OC1A = 11;
const byte pinStartStop = 5;

unsigned int
    AccelProfile[ACCEL_NUMSTEPS] =
    {
        65000,      //32.5mS
        61850,      //30.9mS ...
        58700,
        55550,
        52400,
        49250,
        46100,
        42950,
        39800,
        36650,
        33500,
        30350,
        27200,
        24050,
        20900,
        17750,
        14600,
        11450,
        8300,       //4.15mS
        5150        //2.575mS
                           
    };

unsigned long
    timeSw,
    timeNow;   
byte
    nowSw,
    lastSw,
    accelIdx;
bool
    bRunning;

void setup()
{
    pinMode( pinMEGA_OC1A, OUTPUT );
    digitalWrite( pinMEGA_OC1A, LOW );
    //
    pinMode( pinStartStop, INPUT_PULLUP );
    lastSw = digitalRead( pinStartStop );

    pinMode( LED_BUILTIN, OUTPUT );
    digitalWrite( LED_BUILTIN, LOW );
   
    bRunning = false;
   
    TCCR1A = 0;     //WGM 0, OC1A disconnected from PB1 to start
    TCCR1B = 0;
    TIMSK1 = 0;
    //
    //set timer 1 to tick at 2MHz
    TCCR1B = (1 << CS11);   ///8 = 2MHz clock rate (500nS per tick) - can rescale if need be but constants will need re-calc

    timeSw = 0;

}//setup

void loop()
{   
    timeNow = millis();
    if( timeNow - timeSw >= SW_RD_INTERVAL )
    {
        timeSw = timeNow;
        nowSw = digitalRead( pinStartStop );
        if( nowSw != lastSw )
        {
            lastSw = nowSw;
            if( nowSw == LOW )
            {
                if( !bRunning )
                {
                    StartAccel();
                    bRunning = true;       
                   
                }//if
                else
                {
                    Stop();
                    bRunning = false;       
                   
                }//else
               
            }//if
           
        }//if
       
    }//if

}//loop

void Stop( void )
{
    noInterrupts();
    TCCR1A &= ~(1 << COM1A0 );        //connect pin PB1 to OC1A; set pin on next compare
    TIFR1 &= ~(1 << OCF1A);          //clear any pending interrupt
    TIMSK1 &= ~(1 << OCIE1A);         //enable OC1A interrupt    
    
    PORTB &= 0b01111111;

    interrupts();
   
}//Stop

void StartAccel( void )
{           
    noInterrupts();
    //grab current count and add an arbitrary count to give initial step delay
    OCR1A = TCNT1 + INIT_STEP_DELAY;
    //
    TCCR1A |= (1 << COM1A0 );        //connect pin PB1 to OC1A; set pin on next compare
    TIFR1 |= (1 << OCF1A);          //clear any pending interrupt
    TIMSK1 |= (1 << OCIE1A);         //enable OC1A interrupt
   
    accelIdx = 0;
    PORTB |= 0b10000000;
    
    interrupts();   
   
}//StartAccel

ISR(TIMER1_COMPA_vect)
{
    static unsigned int
        oldOC;
       
    //OC1A occurred; is pin high or low now?
    if( PINB & 0b00100000 )
    {
        //pin has gone high
        
        //digitalWrite( LED_BUILTIN, HIGH );
        
        //save time of OC
        oldOC = OCR1A;
        //set up for width of step pulse
        OCR1A = oldOC + STEP_PULSE_WIDTH;       
        //set to clear the pin on the next compare
        TCCR1A |= (1 << COM1A1 );
        TCCR1A &= ~(1 << COM1A0 );        
   
    }//if
    else
    {
        //pin is low now; set up for next step
        
        //stil accelerating?
        if( accelIdx < ACCEL_NUMSTEPS )
            //yes, use period from acceleration table
            OCR1A = oldOC + AccelProfile[accelIdx++];
        else
            //no, use constant
            OCR1A = oldOC + NORMAL_STEP_TIME;       
       
        //set pin on next compare
        TCCR1A |= (1 << COM1A0 );       
        TCCR1A &= ~(1 << COM1A1 );           

    }//else
   
}//ISR(TIMER1_COMPA_vect)

RayLivingston:
OK, then explain how that would work, with a different delay for EVERY pulse, and precise, to the microsecond, timing of all pulses.

It wouldn't. Sorry to have wasted your time, or caused any offence.

RayLivingston:
Yes, but the next period needs to start time the instant the previous pulse is issued. Hence allowing the counter to keep running, and updating the compare register in the ISR.

you could capture timestamps (micros()) of when the timer is set and when the ISR occurs to determine the latency which can then be subtracted from the next timer value to maintain an absolute time reference. In other words the last period can be programmed to occur exactly w/o accumulating error.

and knowing the latency, you could shorten the timer to anticipate the latency. micros() remains your absolute timebase

The ESP32 has several sources of hardware timershttps://docs.espressif.com/projects/esp-idf/en/latest/api-reference/peripherals/timer.html.

The ESP32 chip contains two hardware timer groups. Each group has two general-purpose hardware timers. They are all 64-bit generic timers based on 16-bit prescalers and 64-bit up / down counters which are capable of being auto-reloaded.

and precise, to the microsecond, timing of all pulses

With this requirement, you will need to turn off the Timer 0 interrupts used to generate millis.

cattledog:
With this requirement, you will need to turn off the Timer 0 interrupts used to generate millis.

Not using the method I outlined. The timer would generate the active edge of every step pulse (and perhaps also the trailing edge, depending on the available timer options, as some will auto-clear the compare pin when the interrupt is actually serviced). Worst case the ISR generates the trailing edge, which is not critical.

Regards,
Ray L.

Idahowalker:
The ESP32 has several sources of hardware timershttps://docs.espressif.com/projects/esp-idf/en/latest/api-reference/peripherals/timer.html.

But I'm not using an ESP32. I'm using either a 328P or a ATTiny1604-0...

Regards,
Ray L.

gcjr:
you could capture timestamps (micros()) of when the timer is set and when the ISR occurs to determine the latency which can then be subtracted from the next timer value to maintain an absolute time reference. In other words the last period can be programmed to occur exactly w/o accumulating error.

and knowing the latency, you could shorten the timer to anticipate the latency. micros() remains your absolute timebase

Using the ISR to capture the timing would introduce FAR too much jitter in the step timing. The step pulses MUST be generated directly by the hardware.

Regards,
Ray L.

PaulRB:
It wouldn't. Sorry to have wasted your time, or caused any offence.

No problem at all. Thanks for trying.

Regards,
Ray L.

RayLivingston:
Using the ISR to capture the timing would introduce FAR too much jitter in the step timing. The step pulses MUST be generated directly by the hardware.

i'm suggesting using timestamps to track the error.

how much is too much jitter?

The only snag I see is that there is no prescale of 16 which you would need for a 1 MHz clock on 16 MHz Arduino. I would use the prescale of 8 (2 MHz clock) and double all of your values. Can you live with a pulse rate no slower than 30.5 Hz?

What happens when the ISR runs out of acceleration data?

Do you ever want to decelerate?