Translating rotary encoder speed into a PWM output, to feed into another arduino as analogInput

Hi!
I am trying to set up an experiment that reads a wheel's turning speed, and depending on whether it's the correct response or not, will reward/punish the subject.
I am using an incremental quadrature encoder (400ppr, model) to measure the rotations of the wheel. The encoder is connected to an Arduino Uno that is running the code below to supposedly read the real-time speed and translate it to pwm output. I then need to take this output and feed it into an Arduino Mega as an analog input, to determine if it was a good enough response (enough speed & duration of movement), and if so, Mega rewards the subject. Mega is necessary for running the whole experiment, as there are other components like solenoid valves, LEDs, piezos etc.

// Encoder output pulse per rotation
#define ENC_COUNT_REV 400 // pins per rotation
#define WHEEL_CIRCUMFERENCE 60 // in cm

// Encoder output to Arduino Interrupt pin
#define  A_PHASE 2
#define  B_PHASE 3

// speed output connected to PWM pin 10
#define PWM 10

unsigned int flag_A = 0;  // Assign a value to the token bit
unsigned int flag_B = 0;  // Assign a value to the token bit

// Pulse count from encoder
volatile long encoderValue = 0;
volatile long lastEncoderValue = 0;

// interval for speed measurements :
float spd_interval = 10; // in ms
float wheelPosition = 0;

// counters for milliseconds during interval
long previousMillis = 0;
long currentMillis = 0;

// Variables for rpm and speed measurements
float rpm = 0;
float spd = 0;

/** *  */
void setup()
{
  Serial.begin(9600);

  pinMode(A_PHASE, INPUT_PULLUP);
  pinMode(B_PHASE, INPUT_PULLUP);

  pinMode(PWM, OUTPUT);

  attachInterrupt(digitalPinToInterrupt(A_PHASE), interrupt, RISING);

  // Setup initial values for timer
  previousMillis = millis();
}

float mapfloat(float x, float in_min, float in_max, float out_min, float out_max)
{
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}


void loop()
{
  // Update rpm value every speed interval
  currentMillis = millis();
  if (currentMillis - previousMillis > spd_interval) {
    previousMillis = currentMillis;

    // Calculate rpm
//    rpm = (float)(encoderValue * 60 / ENC_COUNT_REV);
    spd = ((encoderValue - lastEncoderValue) * ((float)WHEEL_CIRCUMFERENCE / (float)ENC_COUNT_REV)) / (0.001*(float)spd_interval);

    // Only update display when there is a reading
    //if (spd > 0) {
    Serial.print(" SPEED: ");
    Serial.print(spd);
    Serial.print(" cm/s ");
    Serial.print('\t');
    lastEncoderValue = encoderValue;//
    float spd_to_pwm = mapfloat (spd, 0.00, 100.00, 0.00, 255.00);
    spd_to_pwm = min(255,spd_to_pwm);
    spd_to_pwm = max(0,spd_to_pwm);
    analogWrite(PWM, spd_to_pwm);
    Serial.print(spd_to_pwm);
    Serial.println(" pwm");
  } // end if 
} // end loop


void interrupt() // Interrupt function
{
  encoderValue += 1;
  wheelPosition = encoderValue;
}

My issue with the code above is that

  1. I'm not sure if I'm reading speed correctly. I read way too much engineering forums on quadrature encoders, and I now confused myself to death. I want to see a reading in cm/s units, and it seems far too high for what I would expect from the wheel turning. But otherwise it seems like it's working fine.
  2. I don't know how I need to translate it into PWM: I read somewhere that speed of an encoder (in my case cm/s) does not necessarily relate to PWM in a linear fashion. And that's what I am doing here, without any clue as to how to calibrate it etc. for my setup.
  3. I don't know how I can then convert the analog input back into cm/s. Using the same logic? Or is there a more nuanced (and correct) way to do it?

Let me know if I left out any important detail. Hope I can find some smart people with disposable time to help me here! Thanks in advance

It seems you think that a PWM signal is analogue.

Why two processors.
Is the wheel in a different location?
Leo..

I want to have the Mega control the whole experimental structure, the reaction times, learning rate, trial distributions, etc. The wheel (with arduino Uno) is in a box, somewhat distant to all the other stuff.
I don't know the difference between a pulse modulated output and an analog signal yes. I mean I understand that I need some sort of a filter in between to make the signal smoother, but I am honestly learning all this on the fly, as I am a neuroscientist with a psychology background lol

That's a misjudgement. Let the PWM calculation be done by the speed detecting controller and transfer the PWM value to the PWM-ing controller.

1 Like

Am I not currently doing that? The speed detecting controller is the Arduino Uno (that takes in encoder A and B), which is where I do the PWM?

How do I transfer it? Apparently not as an analog input to Arduino Mega, if I'm understanding Wawa's reply correctly..

Correct. Generating true analog calls for either a DAC or a PWM signal filtered to an analog signal using some circuitry.
Try using serial communication transferring a one byte PWM value (0 - 255).

Since the encoder has physical properties, it should produce the same duty cycle independent of the RPM, so it does not produce variable PWM. You need to measure the RPM.

What distance exactly, and what is max wheel RPM.
If slow enough, then it makes more sense to let the Mega read the sensor directly.
Encoding/decoding data is more complex, and it takes some loop time away from the Mega.
There always has to be a very good reason to use two processors.
Leo..

Thanks for all the responses!

Thanks I'll try this.

Is my code correct for that? Because at least for speed, I get quite high values.

Then you shouldn't see my rig haha
The max wheel rpm is 5000 as I see from the datasheet, not sure if it's slow or fast though. The wheel is ~3m away from Mega. I'm also recording electrical signals, and the wheel at it's current state makes pretty bad noise. So I would like to improve that for sure. Mega is running a state machine code for the experiment. How would I add the constant reading of speed in there?

That's max decoder RPM. 5000/60*400 = ~33k RPS.
I asked for the wheel speed, which could be significantly less.

I would try using the encoder (interrupts) directly on the Mega.
There is already a 1m wire on that encoder. Adding another 2m shouldn't make a difference.
Leo..

Hi, I'm back. I have probably a dumb question: can I use the Serial.print() to print stuff on the serial monitor for debugging while sending stuff to Mega over the serial ports?
I'll try taking everything onto Mega soon, I'm waiting for a part.

The data that you send from the Uno to the PC will also go to the Mega.

You can use e.g. SoftwareSerial on the Uno to communicate with the Mega and the normal HardwareSerial to send debug info to the PC. Be aware that SoftwareSerial can not communicate at higher speeds; 19200 should work.

1 Like

Just a side note. How would this approach work?

Instead of having a constant 10 ms time interval for speed measuring, one would record the millis() time of each interrupt.

uint32_t delta_time; // global

void interrupt() // Interrupt function
{
  static uint32_t last_millis;
  uint32_t now;
  now = millis();
  delta_time = now - last_millis;
  last_millis = now;
}

delta_time would hold the reciprocal of the RPM. Or multiply the value with the number of steps your encoder does per revolution.

1 Like

Thank you again for all the responses! I came back with more questions haha

So I did the serial communication, and it seems like it is doing it. But the receiving arduino Mega is doing funny stuff. I have a threshold (SPDTHRESHOLD), I want Mega to deliver reward if the wheel speed goes beyond that. It does that for the first time at the beginning, but then it starts to deliver reward instantly, as if it is reading high speeds from the serial port, even if the wheel is stationary.

Here's the code I use to read the serial port (wheelPort):

void delayCue()
{
  uint16_t  cueDELAY = CUE_DELAY_DUR + random(-300, 300);
  while (!(stateMachine.isDelayComplete(cueDELAY))) {
    if (wheelPort.available())  { // Check to see if at least one character is available
      char ch = wheelPort.read();
      if (ch >= '0' && ch <= '9') // is this an ascii digit between 0 and 9?
      {
        value = (value*10) + (ch - '0');      // accumulate ASCII value converted to numeric value
      }
      else if (ch == 10) { // is the character the newline character 
        serialSpeed = value; // set serialSpeed to the accumulated value
        value = 0;
        Serial.print("Wheel speed: ");
        Serial.println(serialSpeed);
      } // end if ascii
    } // end if wheelPort
    
    // Transitions
    if (serialSpeed > SPDTHRESHOLD) {
      serialSpeed = 0;
      if (goTrial) { // hit
        stateMachine.changeState(deliverReward);
        Serial.println("HIT");
        return;
      }
      else { // false alarm
        Serial.println("FA");
        stateMachine.changeState(timeOut);
        return;
      }
    } // end if speed above threshold
  } // end of delay
  if (stateMachine.isDelayComplete(cueDELAY)) {
    serialSpeed = 0;
    if (goTrial) { // miss
      Serial.println("MISS");
      stateMachine.changeState(iti);
      return;
    }
    else { // correct rejection
      Serial.println("CR");
      stateMachine.changeState(deliverReward);
      return;
    }
  } // end of delay complete
} // end of delayCue State

Not sure if I forgot to clear the variables I read wheelPort into, or something else. Again, thanks a lot for all the help!

Hi again, not sure if I should open another topic, but I wanted to try here one last time.
I figured out what is causing the unintended behavior of Mega, it's serial port overflows. Is there a way to prevent this?
Basically what I am trying to achieve is to read the serial port for a short time (~2 s) every 30 seconds or so, and have the Mega evaluate if the read values are sufficient for the next step. It can do this for the first time that it starts to read serial port, but after then, it overflows and the evaluation gets all messed up.

It's the wrong approach; you should read (and collect) the data when it comes in.

Please post your complete code for both the Uno and the Mega.

You mean every 30 seconds, is it correct? Not every 30 milliseconds?
What are you could to control with such long interval?

Here's the relevant portion of the Mega code:

#include <GOTStateMachine.h>
#include <SoftwareSerial.h>

// Set up a new SoftwareSerial object to read data from the wheel
const byte rxPin = 11;
const byte txPin = 4;
SoftwareSerial wheelPort(rxPin, txPin); // RX_PIN, TX_PIN, inverse_logic (default is false)

//***** State Machine *****
GOTStateMachine stateMachine(50); // execute every 50 milliseconds

int value = 0; // to accumulate serial input
int serialSpeed; // to read Serial input from the wheel

void setup() {
  Serial.begin(9600);
  wheelPort.begin(9600); // to read wheel speed data from the wheel
  stateMachine.setStartState(waitForButton); // Initialise state machine
}

void loop() {
  stateMachine.execute();  // process the states as required
}

void delayCue()
{
  uint16_t  cueDELAY = CUE_DELAY_DUR + random(-300, 300);
  while (!(stateMachine.isDelayComplete(cueDELAY))) {
    if (wheelPort.available())  { // Check to see if at least one character is available
      char ch = wheelPort.read();
      if (ch >= '0' && ch <= '9') // is this an ascii digit between 0 and 9?
      {
        value = (value*10) + (ch - '0');      // accumulate ASCII value converted to numeric value
      }
      else if (ch == 10) { // is the character the newline character 
        serialSpeed = value; // set serialSpeed to the accumulated value
        value = 0;
        Serial.print("Wheel speed: ");
        Serial.println(serialSpeed);
      } // end if ascii
    } // end if wheelPort
    
    // Transitions
    if (serialSpeed > SPDTHRESHOLD) {
      if (goTrial) { // hit
        stateMachine.changeState(deliverReward);
        Serial.println("HIT");
        return;
      }
      else { // false alarm
        Serial.println("FA");
        stateMachine.changeState(timeOut);
        return;
      }
    } // end if speed above threshold
  } // end of delay
  if (stateMachine.isDelayComplete(cueDELAY)) {
    value = 0;
    serialSpeed = 0;
    if (goTrial) { // miss
      Serial.println("MISS");
      stateMachine.changeState(iti);
      return;
    }
    else { // correct rejection
      Serial.println("CR");
      stateMachine.changeState(deliverReward);
      return;
    }
  } // end of delay complete
} // end of delayCue State

And here's the code for the Uno that's connected to the rotary encoder (wheel):

#include <SoftwareSerial.h>
// Encoder output pulse per rotation
#define ENC_COUNT_REV 400
#define WHEEL_CIRCUMFERENCE 60 // in cm
#define RADIUS 9.5 // in cm

// Encoder output to Arduino Interrupt pin
#define  A_PHASE 2
#define  B_PHASE 3

// Set up a new SoftwareSerial object to send data to Mega
const byte rxPin = 8;
const byte txPin = 9;
SoftwareSerial mySerial(rxPin, txPin); // RX_PIN, TX_PIN, inverse_logic (default is false)

unsigned int flag_A = 0;  // Assign a value to the token bit
unsigned int flag_B = 0;  // Assign a value to the token bit

uint32_t delta_time; // global

// Variables for rpm and speed measurements
float rpm = 0;
float spd = 0;

/** *  */
void setup()
{
  // Define pin modes for TX and RX
  pinMode(rxPin, INPUT);
  pinMode(txPin, OUTPUT);
  
  Serial.begin(9600); // hardware serial is for debugging 

  pinMode(A_PHASE, INPUT_PULLUP);
  pinMode(B_PHASE, INPUT_PULLUP);

  // Set the baud rate for the SoftwareSerial object
  mySerial.begin(9600);


  attachInterrupt(digitalPinToInterrupt(A_PHASE), interrupt, RISING);
}

void loop()
{
  float f = (1/(0.001*delta_time)); // per second
  rpm = (float)(f * 60 / ENC_COUNT_REV );
  float w = (rpm * 2 * PI ) / 60; // rad/s
  spd = w * RADIUS; // in cm/s

  int spdToWrite = int(spd);
  mySerial.print(spdToWrite); // print data as an ASCII-encoded decimal
  mySerial.print("\n"); // print newline  (it's a control character to parse data)

  }
}


void interrupt() // Interrupt function
{
  static uint32_t last_millis;
  uint32_t now;
  now = millis();
  delta_time = now - last_millis;
  last_millis = now;
}

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.