Custom receiver code not reading and counting pulses properly

I am trying to create a code that decodes a pulse sequence into commands for a DC motor (with an L293D driver) and a servo. The signal appears as four "synchronization" pulses of approximately 1500 us in width, followed by a "command" sequence of x number of pulses with a pulse width y (between 500 and 1000 us). The amount of command pulses (pulse count, PC) determine the specific "command mode", and the pulse width determines the angle that the servo will rotate to.

After that brief explanation, the problem I'm having is that, according to the serial monitor, not all of the sync pulses are being recognized, meaning the code cannot properly accept the upcoming command pulse sequence. Even without the check that disallows the ISR from registering the command pulse without recognizing all four sync pulses consecutively, the Arduino only seems to read some of the four sync pulses. Note that with an oscilloscope, I have noted that the signal is being sent properly. If the signal is a problem, then it should only be from the frequency (490 Hz), the voltage spike from the rising side of the pulse before it settles to the standard HIGH-state voltage (5 V), or the pulse widths being too small.

If it is from the above possible hardware problems, then I will just change the frequency of the signal to be lower or get a zener diode to limit the spike. But if it is a code problem, then I would like help in trying to figure out the problem. Below is my receiver code (the transmitter code is also attached for reference):

#include <Servo.h>

Servo RCServo; // create a servo object

int receiverPin = 3;
int servoPin = 11;
int motorPinForward = 12;
int motorPinBackward = 13;

// All previous constants which may be necessary for this code

// Pulse Widths
const int centerPW = 750;             // Standard Pulse Width for no turning
const int minPW = 500;                 // Minimum Pulse Width for turning
const int maxPW = 1000;              // Maximum Pulse Width for turning
const int syncPW = 1500;              // Synchronization Segment Pulse Width
const int errorPW = 50;
const int maxAngle = 180;
const int restAngle = 90;
const int minAngle = 0;
const int angleRate = 5;

// Pulse Counts for each mode of operation
const int forwardPC = 15;
const int backwardPC = 40;
const int leftPC = 25;
const int rightPC = 30;
const int forwardLeftPC = 10;
const int backwardLeftPC = 20;
const int forwardRightPC = 35;
const int backwardRightPC = 45;
const int syncPC = 4;

const float period_us = 1000000 / 490;
const float remain = period_us - syncPW;

const float angle_min = ((maxPW - minPW) / (maxSig - minSig)) * (minSig - minSig) + minPW;
const float angle_max = ((maxPW - minPW) / (maxSig - minSig)) * (maxSig - minSig) + minPW;
const float angle_rest = ((maxPW - minPW) / (maxSig - minSig)) * (restSig - minSig) + minPW;

int PW = 0;
int i = 0;
int syncCount = 0;
int prevSyncCount = 0;
int PC = 0;
int duration[45];

void setup() {
  pinMode(servoPin, OUTPUT);
  pinMode(motorPinForward, OUTPUT);
  pinMode(motorPinBackward, OUTPUT);
  pinMode(receiverPin, INPUT);
  RCServo.attach(servoPin);
  
  attachInterrupt(digitalPinToInterrupt(receiverPin), signalCommand, RISING);
  
  Serial.begin(9600);
}

void loop() {
  //motorServoCommand(PC, duration);
}

void signalCommand() {
  //Serial.println("ISR Active");
  
  PW = pulseIn(receiverPin, HIGH); // May not actually measure as the RISING in the attachinterrupt() may cause
                                   // pulseIn() to not register the HIGH

  if (PW >= syncPW - errorPW && PW <= syncPW + errorPW && syncCount < 5) {
    syncCount++;
    if (syncCount == 5) {
      motorServoCommand(PC, duration);
      syncCount = 1;
      PC = 0;
      i = 0;
      for (int j = 0; j < 45; j++) {
        duration[j] = 0;
      }
    }
  }
  else if (PW >= minPW - errorPW && PW <= maxPW + errorPW && syncCount == 4) {
    PC++;
    duration[i] = PW;
    i++;
  }
  else {
    syncCount = 0;
    PC = 0;
    i = 0;
    for (int j = 0; j < 45; j++) {
      duration[j] = 0;
    }
  }

  Serial.print(PW); Serial.print(", ");
  Serial.print(syncCount); Serial.print(", "); Serial.print(PC); Serial.print(", ");
  Serial.print(i); Serial.print(", "); Serial.println(duration[i]);
}

void motorServoCommand(int PC, int duration[]) {
  int ave = 0;
  int k = 0;
  int motorDir = 1; // motorDir: 0 - Back, 1 - None, 2 - Forward
  int turnDir = 1;  // turnDir:  0 - Left, 1 - None, 2 - Right

  //Serial.print("Running Command, ");

  switch (PC) {
    case 15: // Forward
      motorDir = 2;
      turnDir = 1;
      break;
    case 40: // Backward
      motorDir = 0;
      turnDir = 1;
      break;
    case 25: // Left
      motorDir = 1;
      turnDir = 0;
      break;
    case 30: // Right
      motorDir = 1;
      turnDir = 2;
      break;
    case 10: // Forward-Left
      motorDir = 2;
      turnDir = 0;
      break;
    case 20: // Backward-Left
      motorDir = 0;
      turnDir = 0;
      break;
    case 35: // Forward-Right
      motorDir = 2;
      turnDir = 2;
      break;
    case 45: // Backward-Right
      motorDir = 0;
      turnDir = 2;
      break;
    default:
      motorDir = 1;
      turnDir = 1;
      break;
  }

  motorCommand(motorDir);
  servoCommand(turnDir, duration);
}

void motorCommand(int motorDir) {
  if (motorDir == 2) { // Forward
    /*
     * Forward Code
     */
     //Serial.print("Forward, ");
     digitalWrite(motorPinForward, HIGH);
     digitalWrite(motorPinBackward, LOW);
  }
  else if (motorDir == 0) { // Backward
    /*
     * Backward Code
     */
     //Serial.print("Backward, ");
     digitalWrite(motorPinForward, LOW);
     digitalWrite(motorPinBackward, HIGH);
  }
  else { // None
    /*
     * Rest Code
     */
     //Serial.print("Motor Off, ");
     digitalWrite(motorPinForward, LOW);
     digitalWrite(motorPinBackward, LOW);
  }
}

void servoCommand(int turnDir, int duration[]) {
  float avePW = 0.0;
  int k = 0;
  
  for (int j = 0; j < 45 && duration[j] != 0; j++) {
    avePW = avePW + duration[j];
    k = j;
  }
  avePW = avePW / k;
  
  if (turnDir == 2) {
    //float anglePWM_R = ((pw_max - pw_rest) / (maxSig - (restSig + shiftSig))) * (PW_s - (restSig + shiftSig)) + pw_rest;
    int angleR = map(avePW, centerPW, maxPW, restAngle, maxAngle);
    Serial.print(angleR); Serial.println(", ");
    /*
     * Right Code
     */
    for (int pos = restAngle; pos <= angleR; pos = pos + angleRate) {
      RCServo.write(pos);
    }
  }
  else if (turnDir == 0) {
    //float anglePWM_L = ((pw_rest - pw_min) / ((restSig - shiftSig) - minSig)) * (PW_s - minSig) + pw_min;
    int angleL = map(avePW, minPW, centerPW, minAngle, restAngle);
    Serial.print(angleL); Serial.println(", ");
    /*
     * Left Code
     */
    for (int pos = restAngle; pos >= angleL; pos = pos - angleRate) {
      RCServo.write(pos);
    }
  }
  else {
    /*
     * Rest Code
     */
    //Serial.println("No Turning, ");
    RCServo.write(restAngle);
  }
}

EECE4991TransmitterCode.ino (5.2 KB)

I have to say I have not found much joy with the pulseIn and I am not sure it works well in an ISR.
I use the ISR to capture a timer counter. I have used it with speed pulses and for an IR detector which has different pulse counts and widths. If using timer1 then TCNT1. You can adjust the timer with the timer control registers TCCR1A and TCCR1B.
Print out those values and hopefully you will see they coincide with your pulses and then a bit of maths to get all the data you want.

Obviously, the code provided will not compile, but no problem as the concept is clear. 4 digital inputs must be decoded.

IMO, interrupts/pulseIn will likely not give you the flexibility needed beyond perhaps 2 digital signals.
You may find more joy by tightly scanning the 4 pins (not using digital.Read() but you can search for that solution.)
The tight-loop will provide a 1/0 per channel with some sloppiness due to the time shift from pin-to-pin. Sticking the channels into a truth-table will provide a decode table.
I'm too lazy to run the math, but if 16MHz is too sloppy, uC chips are available for Arduino with frequencies an order of magnitude and higher.

PS: buy/build a logic analyzer, it will save you from going bald.

An Arduino Logic Analyzer for the ESP32 - Phil Schatzmann (pschatzmann.ch)

An Arduino Logic Analyzer for the Raspberry Pico using the PIO - Phil Schatzmann (pschatzmann.ch)

Were I approaching this, I would pull out my Saleae logic analyzer and "record". Single-channel O'scope is rather useless unless it contains a 4/8 channel logic-analyzer.

Thank you for the suggestion, but perhaps I was slightly unclear: the digital inputs that have to be decoded are handled by the transmitter code file attached (EECE4991TransmitterCode.ino) as that code will take the forward-backward button and joystick and turn it into a single pulse sequence signal output from only one pin.

From this, the receiver code receives that singular signal on one sole pin and must decode it into the commands relevant to the DC motor and servo. I will absolutely continue to look into your recommendation, but I just wanted to be clear on what I meant. Also, the above code does compile, as I've done it and uploaded it multiple times.

Thank you for the idea. Would it be possible for you to show me a simplistic implementation of that idea, as interrupts and especially timers are still new to me?

No problem. See below. Declare tm1_value as unsigned - better for the maths.
TCCR1B contains prescalar for timer1 which I think you need to change.
Changing it will change the behaviour of the timer
Google it or look at the data sheet.
There a loads of things which you can do by looking at the data sheet for your processor and variable that you can use or set.

and then something like this:
void setup(){
TCCR1A = 0x00; //timer control registers
TCCR1B = 0x84;

void signalCommand() { //timer1 capture
tm1_value = TCNT1;
and then use tm1_value for whatever you want.
Print it to see what's going on when it's triggered
Generate a last times value and subtract and you have a pulse period.
Good luck

The pulseIn() call in the ISR is going to ignore the rest of the current pulse and wait until the start and end of the next pulse.

Change the interrupt mode to “CHANGE” and read the pin to see if you are at the start of a pulse (HIGH) or end at the end of a pulse (LOW). Record micros() at both ends to get length of pulse and time between pulses.

Thanks for the idea. I have attempted an implementation of it, and it works for the first few pulses, but strangely begins to go off the rails after a bit. Looking at the code, do you think you could help to figure out why?

Ignore the majority of the constants and all the methods after signalCommand(). The only constant that are relevant are minPW, maxPW, centerPW, syncPW, and errorPW.

#include <Servo.h>

Servo RCServo; // create a servo object

int receiverPin = 3;
int servoPin = 11;
int motorPinForward = 12;
int motorPinBackward = 13;

// All previous constants which may be necessary for this code

// Pulse Widths
const int centerPW = 1250;             // Standard Pulse Width for no turning
const int minPW = 1000;                 // Minimum Pulse Width for turning
const int maxPW = 1500;              // Maximum Pulse Width for turning
const int syncPW = 2500;              // Synchronization Segment Pulse Width
const int errorPW = 50;
const float maxSig = 1023.0;        // Joystick minimum and maximum serial outputs
const float minSig = 0.0;
const float restSig = 501.0;        // Resting joystick serial output
const float shiftSig = 30.0;        // Serial output shift of joystick to apply deadzone
const int maxAngle = 180;
const int restAngle = 90;
const int minAngle = 0;
const int angleRate = 5;

// Pulse Counts for each mode of operation
const int forwardPC = 15;
const int backwardPC = 40;
const int leftPC = 25;
const int rightPC = 30;
const int forwardLeftPC = 10;
const int backwardLeftPC = 20;
const int forwardRightPC = 35;
const int backwardRightPC = 45;
const int syncPC = 4;

const float period_us = 1000000 / 490;
const float conversion = 255; // Number gotten to convert Duty Cycle percentages to analogWrite() output
const float remain = period_us - syncPW;

const float angle_min = ((maxPW - minPW) / (maxSig - minSig)) * (minSig - minSig) + minPW;
const float angle_max = ((maxPW - minPW) / (maxSig - minSig)) * (maxSig - minSig) + minPW;
const float angle_rest = ((maxPW - minPW) / (maxSig - minSig)) * (restSig - minSig) + minPW;

int PW = 0;
int i = 0;
int syncCount = 0;
int prevSyncCount = 0;
int PC = 0;
int duration[45];
int state = 0;
int time_i = 0.0;
int time_f = 0.0;

void setup() {
  pinMode(servoPin, OUTPUT);
  pinMode(motorPinForward, OUTPUT);
  pinMode(motorPinBackward, OUTPUT);
  pinMode(receiverPin, INPUT);
  RCServo.attach(servoPin);
  
  attachInterrupt(digitalPinToInterrupt(receiverPin), pulseWidth, CHANGE);
  
  Serial.begin(9600);
}

void loop() {
  //motorServoCommand(PC, duration);
}

void pulseWidth() {
  if (state == 0) {
    state = 1;
    time_i = micros();
  }
  else {
    state = 0;
    time_f = micros() - time_i;
    signalCommand(time_f);
  }
}

void signalCommand(int PW) {
  //Serial.println("ISR Active");
  
  //PW = pulseIn(receiverPin, HIGH/*, 1000000*/); // May not actually measure as the RISING may cause
                                                // pulseIn() to not register the HIGH
  time_i = 0;
  time_f = 0;

  if (PW >= syncPW - errorPW && PW <= syncPW + errorPW && syncCount < 5) {
    syncCount++;
    if (syncCount == 5) {
      motorServoCommand(PC, duration);
      syncCount = 1;
      PC = 0;
      i = 0;
      for (int j = 0; j < 45; j++) {
        duration[j] = 0;
      }
    }
  }
  else if (PW >= minPW - errorPW && PW <= maxPW + errorPW && syncCount == 4) {
    PC++;
    duration[i] = PW;
    i++;
  }
  else {
    syncCount = 0;
    PC = 0;
    i = 0;
    for (int j = 0; j < 45; j++) {
      duration[j] = 0;
    }
  }

  Serial.print(PW); Serial.print(", ");
  Serial.print(syncCount); Serial.print(", "); Serial.print(PC); Serial.print(", ");
  Serial.print(i); Serial.print(", "); Serial.println(duration[i]);
}

void motorServoCommand(int PC, int duration[]) {
  int ave = 0;
  int k = 0;
  int motorDir = 1; // motorDir: 0 - Back, 1 - None, 2 - Forward
  int turnDir = 1;  // turnDir:  0 - Left, 1 - None, 2 - Right

  //Serial.print("Running Command, ");

  switch (PC) {
    case 15: // Forward
      motorDir = 2;
      turnDir = 1;
      break;
    case 40: // Backward
      motorDir = 0;
      turnDir = 1;
      break;
    case 25: // Left
      motorDir = 1;
      turnDir = 0;
      break;
    case 30: // Right
      motorDir = 1;
      turnDir = 2;
      break;
    case 10: // Forward-Left
      motorDir = 2;
      turnDir = 0;
      break;
    case 20: // Backward-Left
      motorDir = 0;
      turnDir = 0;
      break;
    case 35: // Forward-Right
      motorDir = 2;
      turnDir = 2;
      break;
    case 45: // Backward-Right
      motorDir = 0;
      turnDir = 2;
      break;
    default:
      motorDir = 1;
      turnDir = 1;
      break;
  }

  motorCommand(motorDir);
  servoCommand(turnDir, duration);
}

void motorCommand(int motorDir) {
  if (motorDir == 2) { // Forward
    /*
     * Forward Code
     */
     //Serial.print("Forward, ");
     digitalWrite(motorPinForward, HIGH);
     digitalWrite(motorPinBackward, LOW);
  }
  else if (motorDir == 0) { // Backward
    /*
     * Backward Code
     */
     //Serial.print("Backward, ");
     digitalWrite(motorPinForward, LOW);
     digitalWrite(motorPinBackward, HIGH);
  }
  else { // None
    /*
     * Rest Code
     */
     //Serial.print("Motor Off, ");
     digitalWrite(motorPinForward, LOW);
     digitalWrite(motorPinBackward, LOW);
  }
}

void servoCommand(int turnDir, int duration[]) {
  float avePW = 0.0;
  int k = 0;
  
  for (int j = 0; j < 45 && duration[j] != 0; j++) {
    avePW = avePW + duration[j];
    k = j;
  }
  avePW = avePW / k;
  
  if (turnDir == 2) {
    //float anglePWM_R = ((pw_max - pw_rest) / (maxSig - (restSig + shiftSig))) * (PW_s - (restSig + shiftSig)) + pw_rest;
    int angleR = map(avePW, centerPW, maxPW, restAngle, maxAngle);
    Serial.print(angleR); Serial.println(", ");
    /*
     * Right Code
     */
    for (int pos = restAngle; pos <= angleR; pos = pos + angleRate) {
      RCServo.write(pos);
    }
  }
  else if (turnDir == 0) {
    //float anglePWM_L = ((pw_rest - pw_min) / ((restSig - shiftSig) - minSig)) * (PW_s - minSig) + pw_min;
    int angleL = map(avePW, minPW, centerPW, minAngle, restAngle);
    Serial.print(angleL); Serial.println(", ");
    /*
     * Left Code
     */
    for (int pos = restAngle; pos >= angleL; pos = pos - angleRate) {
      RCServo.write(pos);
    }
  }
  else {
    /*
     * Rest Code
     */
    //Serial.println("No Turning, ");
    RCServo.write(restAngle);
  }
}

Thank you, but would it be possible for you to expand on that a little, in a way that remains applicable to my scenario?

That's for you to do. It's the fun of Arduino.

I would normally agree, except when completion time is a factor. And that completion date is coming closer and closer.

Thank you all for your suggestions. After a lot of trial and error, I found out that the usage of interrupts like @johnwasser suggested was the best, but the problem I was having with using his idea was how to properly communicate variables between the ISR and the standard loop. This is because the program doesn't know when the ISR is going to be called at any given moment, so data used in both sections will likely confuse the program without declaring the variables as volatile. The reason for this, from what I can gather, is that the compiler will try to "improve" or optimize the code for you, and variables being used in an ISR and the standard loop will cause it to try and force an optimization, unless you declare it as volatile. From that, a "reset" was needed so as to allow the program to have some base for which to function. After all of that, there was just logical bugs to fix. Below is my final code:

#include <Servo.h>

Servo RCServo; // create a servo object

int receiverPin = 3;
int servoPin = 11;
int motorPinForward = 12;
int motorPinBackward = 13;
int motorEnable = 8;

// Pulse Widths
const int centerPW = 1000;             // Standard Pulse Width for no turning
const int minPW = 500;                 // Minimum Pulse Width for turning
const int maxPW = 1500;              // Maximum Pulse Width for turning
const int syncPW = 2500;              // Synchronization Segment Pulse Width
const int errorPW = 75;
const float maxAngle = 180;
const int restAngle = 90;
const float minAngle = 0;

// Pulse Counts for each mode of operation
const int forwardPC = 6;
const int backwardPC = 16;
const int leftPC = 10;
const int rightPC = 12;
const int forwardLeftPC = 4;
const int backwardLeftPC = 14;
const int forwardRightPC = 8;
const int backwardRightPC = 18L;
const int syncPC = 4;

const unsigned long period_us = 1000000L / 500L;
const unsigned long remain = period_us - syncPW;
const int syncGap = 500;

int PW = 0;
int syncCount = 0;
volatile int PC = 0;
int state = 0;
int time_i = 0;
int time_f = 0;
volatile int motorDir = 1; // motorDir: 0 - Back, 1 - None, 2 - Forward
volatile int turnDir = 1;  // turnDir:  0 - Left, 1 - None, 2 - Right
volatile int reset_i = 0;
int reset_f;
int maxTime = 0.001*(4*(syncPW + syncGap) + backwardRightPC*period_us + 2000);
volatile int totalPW = 0;

void setup() {
  pinMode(servoPin, OUTPUT);
  pinMode(motorPinForward, OUTPUT);
  pinMode(motorPinBackward, OUTPUT);
  pinMode(motorEnable, OUTPUT);
  pinMode(receiverPin, INPUT);
  RCServo.attach(servoPin);
  
  attachInterrupt(digitalPinToInterrupt(receiverPin), pulseWidth, CHANGE);
  
  Serial.begin(115200);
}

void loop() {
  int currMotorCommand, currServoCommand, currModePC;
  int currTotalPW;
  
  noInterrupts();
  if (digitalRead(receiverPin) == LOW) {
    reset_f = millis() - reset_i;
  }
  
  currMotorCommand = motorDir;
  currServoCommand = turnDir;
  currTotalPW = totalPW;
  currModePC = PC; 
  
  if (digitalRead(receiverPin) == LOW && reset_f >= maxTime) {
    motorDir = 1;
    turnDir = 1;
    totalPW = 0;
    PC = 0;
  }
  interrupts();

  motorCommand(currMotorCommand);
  servoCommand(currServoCommand, currTotalPW, currModePC);
}

void pulseWidth() {
  if (state == 0 && digitalRead(receiverPin) == HIGH) {
    state = 1;
    time_i = micros();
  }
  else if (state == 1 && digitalRead(receiverPin) == LOW) {
    state = 0;
    time_f = micros() - time_i;
    reset_i = millis();
    signalCommand(time_f);
  }
}

void signalCommand(int PW) {
  if (PW >= syncPW - errorPW && PW <= syncPW + errorPW && syncCount < 5) {
    syncCount++;
    if (syncCount == 5) {
      motorServoCommand(PC);
      totalPW = 0;
      syncCount = 1;
      PC = 0;
    }
  }
  else if (PW >= minPW - errorPW && PW <= maxPW + errorPW && syncCount == 4) {
    PC++;
    totalPW = totalPW + PW;
  }
  else {
    syncCount = 0;
    PC = 0;
    totalPW = 0;
  }
}

void motorServoCommand(int PC) {
  switch (PC) {
    case 6: // Forward
      motorDir = 2;
      turnDir = 1;
      break;
    case 16: // Backward
      motorDir = 0;
      turnDir = 1;
      break;
    case 10: // Left
      motorDir = 1;
      turnDir = 0;
      break;
    case 12: // Right
      motorDir = 1;
      turnDir = 2;
      break;
    case 4: // Forward-Left
      motorDir = 2;
      turnDir = 0;
      break;
    case 14: // Backward-Left
      motorDir = 0;
      turnDir = 0;
      break;
    case 8: // Forward-Right
      motorDir = 2;
      turnDir = 2;
      break;
    case 18: // Backward-Right
      motorDir = 0;
      turnDir = 2;
      break;
    default:
      motorDir = 1;
      turnDir = 1;
      break;
  }
}

void motorCommand(int motorD) {
  switch (motorD) {
    case 2:
      digitalWrite(motorEnable, HIGH);
      digitalWrite(motorPinForward, HIGH);
      digitalWrite(motorPinBackward, LOW);
      break;
    case 0:
      digitalWrite(motorEnable, HIGH);
      digitalWrite(motorPinForward, LOW);
      digitalWrite(motorPinBackward, HIGH);
      break;
    default:
      digitalWrite(motorEnable, LOW);
      digitalWrite(motorPinForward, LOW);
      digitalWrite(motorPinBackward, LOW);
      break;
  }
 
}

void servoCommand(int turnD, int total, int mPC) {
  float average;
  float angle;

  if (turnD == 1) {
    average = centerPW;
  }
  else if (turnD == 2 && (mPC == 12 || mPC == 8 || mPC == 18)) {
    average = total / mPC;
  }
  else if (turnD == 0 && (mPC == 10 || mPC == 4 || mPC == 14)) {
    average = total / mPC;
  }

  angle = 180 - (((maxAngle - minAngle) / (maxPW - minPW))*(average - minPW) + minAngle);
  RCServo.write(angle);
}