Setting non-linear steps from rotary encoder for motor speed control

Hi - I have started a project to build a model train controller for my father. I am building 3 x speed controllers and various points controllers, level crossing sensors etc.

The speed controllers will be using a PWM motor control module form Pololu that I need to drive. I have decided to use rotary encoders for the user input to the speed controllers and have prototyped the hardware debounce using a RC circuit and inverting Schmitt trigger (74HC14) for each encoder output which is working perfectly with the Buxtronix state table rotary encoder library -
arduino/libraries/Rotary at master · buxtronix/arduino · GitHub.

The code I have written so far is just test code and absolutely zero refactoring has taken place. I know it needs a lot of work !!!

I have been going at this for a few days now and I am stuck on creating a non-linear step for each corresponding rotary encoder step based on rotation speed of the encoder.

I basically want to increase at a very fine step for slow turns but as the user increases the turn rate I want to ramp up the motor speed in a non-linear fashion.

I haven't got the Pololu speed controller as yet so I am just mocking this up with numbers (0-254) which will eventually used with analogWrite() to generate a PWM signal.

The code runs Timer1 on a Teensy++ 2.0 (will be moving to a more powerful Teensy 3.6 later) at 1kHz and attaches a timer interrupt to poll the rotary encoder. If a state change is detected the main loop will then look at the value and every 50ms looks at how many steps the encoder has incremented / decremented and divides by time to get an encoder speed.

This is where I am stuck ! I need to turn this speed into a step value other than "1". If the speed of user rotation increases I want to step up faster.

The calculations I am using may be totally wrong, I am just going around in circles here which is why I am asking for some help.

#include <Rotary.h>
#include <TimerOne.h>

Rotary rotary1 = Rotary(0, 1);

void setup() {
  Timer1.initialize(1000); //1000us = 1ms = 0.001s = 1kHz
  Timer1.attachInterrupt(process_encoders); 
  Serial.begin(57600);
}

typedef struct {
    int state = 0;
    volatile int value = 0;
    long time_old;
    int value_old;
    float encoder_speed;
    long steps;
    int speed_value = 0; //speed value to be used for motor controller PWM
    byte direction = 1; //0 = CW / 1 = CCW
} encoder_struct;

encoder_struct encoder1;

int speed_check_period = 50; //millis
int pb_counter = 0;

void loop() {
    
  if (encoder1.state == 1) { //only run if the timer interrupt has flagged a state change
    //Serial.print("Value: ");
    //Serial.println(encoder1.value);

    //check speed every 50ms rather than on each loop
    cli(); //stop interrupts happening before calculation
    if((millis() - encoder1.time_old) >= speed_check_period) {  //millis rollover ??
      
      encoder1.steps = abs(encoder1.value - encoder1.value_old); //number of encoder steps triggered since last speed check
      //Serial.print("Steps since last check: ");
      //Serial.println(encoder1.steps);

      encoder1.encoder_speed = (((float)encoder1.steps / (float)(millis() - encoder1.time_old)) * 1000); //using a 1000 multiplier
      
      Serial.print("Encoder Speed: ");
      Serial.println(encoder1.encoder_speed);

      // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      // create a non-linear step value based on speed to increment encoder1.speed_value
      
      if(encoder1.direction == 1) { //turning clockwise
        encoder1.speed_value = constrain((encoder1.speed_value + 1),0,254);
      } else {
        encoder1.speed_value = constrain((encoder1.speed_value - 1),0,254);
      }
      
      Serial.print("Speed1: ");
      Serial.println(encoder1.speed_value);

      // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      
      encoder1.time_old = millis();
      encoder1.value_old = encoder1.value;
    }

    sei(); //restart interrupts

    encoder1.state = 0;
  }

}

static void process_encoders() {  //read encoder values using timer interrupt
  unsigned char result = rotary1.process();
  if (result == DIR_CW) {
    encoder1.value++;
    encoder1.direction = 1;
    encoder1.state = 1; //flag the struct as being changed
  } else if (result == DIR_CCW) {
    encoder1.value--;
    encoder1.direction = 0;
    encoder1.state = 1; //flag the struct as being changed
  }
}

The serial output from some testing is below, the smaller "Encoder Speed" values are when I turn the encoder slowly and then I twist it as fast as I can to get the upper value. Each time I am only increasing "Speed1" by one as I don't yet have code to smoothly increase to 254 based on "Encoder Speed":

Encoder Speed: 0.39
Speed1: 1
Encoder Speed: 1.86
Speed1: 2
Encoder Speed: 1.62
Speed1: 3
Encoder Speed: 1.41
Speed1: 4
Encoder Speed: 0.95
Speed1: 5
Encoder Speed: 22.22
Speed1: 6
Encoder Speed: 1.16
Speed1: 7
Encoder Speed: 34.48
Speed1: 8
Encoder Speed: 53.57
Speed1: 9
Encoder Speed: 75.47
Speed1: 10

Here is my prototype !

    //check speed every 50ms rather than on each loop
    cli(); //stop interrupts happening before calculation
    if((millis() - encoder1.time_old) >= speed_check_period) {

Why are you stopping and restarting interrupt handling on EVERY pass through loop()? You should do that ONLY when it is time to copy the variables used in the ISR and ONLY for long enough to copy the values.

Before you can map encoder speed to an amount to increment, you need to define what that relationship is. Draw a graph, post a table, something to show how much you want to increment by when the speed is x.

PaulS:
Before you can map encoder speed to an amount to increment, you need to define what that relationship is. Draw a graph, post a table, something to show how much you want to increment by when the speed is x.

Thats where I am stuck.... I don't quite know how much I want to increment by. I simply don't want 150 turns of the rotary encoder to get to 100% PWM.

I just played around with this code (I found on another site):

      int step = 1<<(int(encoder1.encoder_speed/9));
      
      if(encoder1.direction == 1) { //turning clockwise
        encoder1.speed_value = constrain((encoder1.speed_value + step),0,254);
      } else {
        encoder1.speed_value = constrain((encoder1.speed_value - step),0,254);
      }

It seems to gives output much closer to what I am looking for:

Encoder Speed: 0.00
Speed1: 1
Encoder Speed: 2.67
Speed1: 2
Encoder Speed: 1.01
Speed1: 3
Encoder Speed: 8.62
Speed1: 4
Encoder Speed: 1.26
Speed1: 5
Encoder Speed: 127.27
Speed1: 254

A few slow turns incremented by 1 and a quick flick of the encoder gave me 100% / 254.

I have to admit I haven't fully worked out how my new 'step' variable is getting calculated.

I would go about this task very differently, and (I think) a lot more simply.

Rather than use a timer to check the encoder I would set up the encoder so that every pulse causes an interrupt. And then in the Interrupt Service Routine (ISR) I would increment or decrement the encoder count, save the value of micros() and set a variable (let's call it newPulse) to true. Actually if pulses come from two encoder pins you may need two ISRs - but they will be almost identical.

In my main code I would be checking if newPulse == true and when it is I would compare the new encoder count with the previous one to know which direction and I would compare the new saved value of micros() with the previous saved value to get the time between pulses. Then you have all the data you need to update other parts of the program.

...R

Yes, this is probably better than a timer based interrupt.