Generating a three state pulse sequence on two lines

I'm controlling a mechanical mileometer that needs a sequence of pulses on two lines. Its working, but I feel the code isnt very good. Is there a "nicer" way to do this?

My first idea was to use a state machine, but it just made the code longer.

void doStep() {
  unsigned long t0, t1, t2, td = 30;  //timing pulse duration; minimum of about 60msec.
  t0 = millis();                      //start of pulse
  digitalWrite(phase1, HIGH);         //high turns on current to coil via transistor on driver board
  digitalWrite(phase2, LOW);
  t2 = millis();
  while ((t2 - t0) < td) { t2 = millis(); }  //wait until first phase is complete, then start phase 2
  t1 = millis();                             //start of second phase
  digitalWrite(phase1, LOW);
  digitalWrite(phase2, HIGH);
  t2 = millis();
  while ((t2 - t1) < td) { t2 = millis(); }  //wait until phase 2 is complete
  //save power in motor
  digitalWrite(phase1, LOW);
  digitalWrite(phase2, LOW);
}

As your function uses blocking while loops for timing you might just as well use delay() and make the code simpler

Keep it simple then

void doStep() {
  pinMode(phase1, OUTPUT);
  pinMode(phase2, OUTPUT);

  digitalWrite(phase1, HIGH);  
  digitalWrite(phase2, LOW); // likely unnecessary as default state will be low
  delay(30);

  digitalWrite(phase1, LOW);
  digitalWrite(phase2, HIGH);
  delay(30);

  digitalWrite(phase2, LOW);
}

Don’t forget to set the mode

Having that in setup means it will be done only once.

Yes, it probably would be longer. But at least it would not be blocking code.

Question is, in your project, is blocking code actually a problem?

It would be if it used while loops or delay() :grinning:

Just to clarify that for other readers...

Using while loops does not, in itself, mean the code is blocking code.

While loops that execute quickly are ok. For example a while loop that searches an array for a given value, or checks several buttons to see if any are pressed. But often it's more appropriate to use a for loop rather than a while loop in these situations.

But a while loop that doesn't finish until it has waited for something to happen, like millis() reaching a value, or waiting until a button is pressed or a character is received from Serial, that's blocking code.

Paul, you are correct. I expressed it badly

What I should have said was

it would be if it used blocking while loops or delay()

...
The one place I always use while loops is to clear out the serial buffer. If the rest of the code is well written and non-blocking then there's likely to be only a small number of characters anyway, so:

while (Serial.available > 0) {
// Get the character and store it somewhere
}

@johnerrington

How about this:

bool pulsesOn = false;
constexpr uint8_t phase1 = 2; // Edit for whichever are the correct pins
constexpr uint8_t phase2 = 3; // Edit for whichever are the correct pins

void setup() {
    pinMode(phase1, OUTPUT);
    pinMode(phase2, OUTPUT);
    digitalWrite(phase1, LOW);  // I'm assuming low is the desired idle state
    digitalWrite(phase2, LOW);
}

void loop() {
    pulses();
}

void pulses() {
    uint32_t currentMillis = millis();
    static uint32_t lastMillis;
    constexpr uint32_t pulseLength = 30;
    static uint8_t state = 0;
    
    switch (state) {
        case 0:
            if (pulsesOn) {
                pulsesOn = false;
                lastMillis = currentMillis;
                digitalWrite(phase1, HIGH);  
                digitalWrite(phase2, LOW);
                state = 1;
            }
            break;
        case 1:
            if (currentMillis - lastMillis >= pulseLength) {
                lastMillis = currentMillis;
                digitalWrite(phase1, LOW);
                digitalWrite(phase2, HIGH);
                state = 2;
            }
            break;
        case 2:
            if (currentMillis - lastMillis >= pulseLength) {
                digitalWrite(phase2, LOW);
                state = 0;
             }
             break;
        default:
            state = 0;
            pulsesOn = false;
            break;
    }
}

In order to trigger the pulses somewhere in your other code you do:

pulsesOn = true;

This code assumes all other code is non-blocking and loop executes in under 30ms, a lot less than 30ms, which is not hard to achieve with well written, non-blocking code.

Code compiles for Nano Every, I've no reason to think it won't compile for any other board. Not tested.

I think this serves as a reminder of why it is beginners find it hard to get their heads around non-blocking code and why they should avoid delay.

Knowing the answer to this question is vital

what microcontroller are you using?
if an ESP32S3 have a look at the Remote Control Transceiver (RMT) which can be used to generate multiple synchronized pulse sequences

Hello johnerrington

Consider this example:

//https://forum.arduino.cc/t/generating-a-three-state-pulse-sequence-on-two-lines/1401049
//https://europe1.discourse-cdn.com/arduino/original/4X/7/e/0/7e0ee1e51f1df32e30893550c85f0dd33244fb0e.jpeg
#define ProjectName "Generating a three state pulse sequence on two lines"
#define NotesOnRelease "Arduino Mega Tested"

// make names
enum TimerEvent {NotExpired, Expired};
enum TimerControl {Halt,Run};
enum OnOff {Off, On};

// make variables
constexpr uint8_t phases[]
{
  0b00000001,
  0b00000010,
  0b00000000,
};
constexpr uint32_t phaseTime {1000}; 
constexpr uint8_t phasePins[] {9,10}; 

// make structures
struct TIMER
{
  uint32_t interval;
  uint8_t control;
  uint32_t now;
  uint8_t expired(uint32_t currentMillis)
  {
    uint8_t timerEvent = currentMillis - now >= interval and control;
    if (timerEvent == Expired) now = currentMillis;
    return timerEvent;
  }
};
TIMER phase {phaseTime, Run, 0};
// make support
void heartBeat(const uint8_t LedPin, uint32_t currentMillis)
{
  static bool setUp = false;
  if (setUp == false) pinMode (LedPin, OUTPUT), setUp = true;
  digitalWrite(LedPin, (currentMillis / 500) % 2);
}
// make application

void setup()
{
  Serial.begin(115200);
  Serial.print("Source: "), Serial.println(__FILE__);
  Serial.print(ProjectName), Serial.print(" - "), Serial.println(NotesOnRelease);
  
  for (auto phasePin:phasePins) 
  {
    pinMode(phasePin,OUTPUT);
    digitalWrite(phasePin,On);
    delay(1000);
    digitalWrite(phasePin,Off);
    delay(200);
  }
  
  Serial.println(" =-> and off we go\n");
}
void loop()
{
  uint32_t currentMillis = millis();
  heartBeat(LED_BUILTIN, currentMillis);
  if (phase.expired(currentMillis) == Expired)
  {
    static uint8_t phaseStep=0;
    uint8_t element=0;
    for (auto phasePin:phasePins) digitalWrite(phasePin,bitRead(phases[phaseStep],element++));
    phaseStep=(phaseStep+1) %sizeof(phases);
  }
}

Thanks to all of you for your very helpful comments.

Therev are often many sometimes conflicting factors involved in deciding what is “good” code.

It may to be correct, bug free, robust, efficient, maintainable ….

In this case the main requirements were for it to meet the needs of the end user; and for me to be able to understand it if it later needs amending.

While @PerryBebbington state machine solution isnt compact it is comprehensible and non-blocking, which has enabled me to do filtering on the data from the interrupt.

Thanks also @paulpaulson for your sketch which will give me a lot to think about. Its always interesting to see an alternative way of thinking about a problem.

However - a puzzle:

at a tSpeed above 60 the result of this line falls to zero. If I Serial.print milesPulsePeriod after this line, it does show as zero yet oneHour = 3,600,000; tSpeed = 61; milesCal=541

so the value SHOULD be 109. not zero.

at that speed the incoming pulses are at atound 900usec so I dont see that interrupts should be causing the issue. Any ideas?

  milesPulsePeriod = oneHour / (tSpeed * milesCal);  //milliseconds

Just for context here is my “final” (hopefully) sketch

/*
  speedoFinal_state: drives speedo and odometer at a rate controlled by pulse input frequency
  ALL WORKING using micros timing for input pulses on interrupt.
  calculation: 0.1 mile = 55 wheel rotations at 3m rolling circumference
  = 330 shaft rotations with a 6:1 final drive
  and assume 20 sensor pulses per shaft rotation = 6600 pulses per 0.1 mile
  on test the mileometer required 541 pulses = 1 mile.  Due to mechanics likely true ratio is 540 as that factorises to match drive motor and gears
*/

//pin assignments
const byte speedoPin = 5;  //pwm out to speedo  MUST BE PWM PIN 5 or 6
const byte senderPin = 2;  // pulse interrupt input  MUST BE INTERRUPT PIN
const int phase1 = 6;
const int phase2 = 7;

//  calibration constants: nb may need adjusting if not enough resolution wtih single digit integer values
//  change these values to match the actual constant factors for the bus.
const int K = 9;   // wheel rotations a minute at 1mph
const int D = 6;   //final drive ratio
const int S = 20;  //sender  drive ratio (a guess for now)

int F;                  // F = K * D * S  speed factor is pulses / min at 1mph  so mph = ppm / F : 
//with these values F is 1080
float iSpeedCal = 3.2;  // 255 = 80mph so 1mph = 255/80
int milesCal = 541;     //  calibrated to true speed: mileometer registers 1 mile for 541 pulses.

volatile boolean iFlag = false;
volatile unsigned long tNow, tDiff = 100000, tPrev;  //millis timing on interrupt to measure pulse period
unsigned long ppMin, ppMinOld, oneMin = 60000000;    //60 sec 60000msec, 60,000,000 microseconds
int tSpeed, iSpeed;                                  //true speed mph and indicated speed pwm value to galvo
unsigned long oneHour = 3600000;                     //in milliseconds, for mileometer pulse period

unsigned long tStep, tStep1, milesPulsePeriod;  //managing the mileometer - unsigned long because using millis timing
int state = 2;                                  //mileometer output state
boolean stepOn = false;

unsigned long tPrint, tPrintOld, tPrintWait = 1000;  //only print once a second
char buffer[120];

void freqP() {  //interrupt service routine
  tNow = micros();
  tDiff = tNow - tPrev;  //tDiff is the pulse period in usec
  tPrev = tNow;
  iFlag = true;
}

void setup() {
  Serial.begin(115200);  // note sensible baud rate
  Serial.println("comms up");
  pinMode(phase1, OUTPUT);
  pinMode(phase2, OUTPUT);
  pinMode(speedoPin, OUTPUT);  // pwm
  attachInterrupt(digitalPinToInterrupt(senderPin), freqP, RISING);
  tNow = millis();
  ppMinOld = 1;
  F = K * D * S;
  Serial.print("F is ");
  Serial.println(F);
}


void loop() {

  if (iFlag) {
    ppMin = oneMin / tDiff;  //pulses per minute
    //filter
    ppMin = (ppMin + (63 * ppMinOld)) / 64;  //careful not to exceed size of long unsigned integer
    ppMinOld = ppMin;
    iFlag = false;
  }

  //calculate true speed
  tSpeed = ppMin / F;  // in mph based on F = K*D*S

  //operate the speedo
  iSpeed = int(tSpeed * iSpeedCal);
  analogWrite(speedoPin, iSpeed);  //max value for speed is 255: max iSpeed is 60

  milesPulsePeriod = oneHour / (tSpeed * milesCal);  //milliseconds

  //operate the mileometer
  tStep = millis();
  if ((tStep - tStep1) >= milesPulsePeriod) {  //if itstime to start a new pulse ..
    tStep1 = tStep;
    //do a pulse
    stepOn = true;
  }

  doStep();

  //if its time to print
  tPrint = millis();
  if ((tPrint - tPrintOld) > tPrintWait) {
    sprintf(buffer, "sender pulse interval is : %lu usec: sender pulses per minute : %lu  Speed is %d mph: milesPulsePeriod is: %lu usec\n", tDiff, ppMin, tSpeed, milesPulsePeriod);
    Serial.print(buffer);
    tPrintOld = tPrint;
  }
}


void doStep() {
  uint32_t currentMillis = millis();
  static uint32_t lastMillis;
  constexpr uint32_t pulseLength = 30;  //a 30msec phase pulse seems to work well with the mechanical mileometer

  switch (state) {
    case 0:
      if (stepOn) {
        stepOn = false;
        lastMillis = currentMillis;
        digitalWrite(phase1, HIGH);
        digitalWrite(phase2, LOW);
        state = 1;
      }
      break;
    case 1:
      if (currentMillis - lastMillis >= pulseLength) {
        lastMillis = currentMillis;
        digitalWrite(phase1, LOW);
        digitalWrite(phase2, HIGH);
        state = 2;
      }
      break;
    default:
      if (currentMillis - lastMillis >= pulseLength) {
        digitalWrite(phase2, LOW);
        state = 0;  //ready for next step
      }
      break;
  }
}

It´s a pleasure to help.

Postscript:

  • arrays, enums and structs are your friends.

Yes,
I've got an idea, and solution.

tspeed and miles Cal are data type 'int'.

The problem occurs when (tspeed * milesCal) becomes greater than the maximum value an int can hold and overflows, i.e it becomes negative.

Thus the result of milesPulsePeriod = oneHour / (tSpeed * milesCal); becomes negative.
However milesPulsePeriod is unsigned long - it is unable to represent a negative number.

60 * 541 = 32460 :slightly_smiling_face:
61 * 541 = 33001 :frowning:

Here is a screenshot of the serial monitor that illustrates the problem:

By the way John, the serial printing says that milesPulsePeriod is measured in microseconds, it should be milliseconds.

If you change the datatype of tspeed and milesCal to 'unsigned int' the problem goes away:

So it does. Thanks @JohnLincoln

I was looking at the “nearly” 100ms duration of the pulse routine, and the interrupts, forgot to check the size of the product of 2 ints.

As you say I’ve changed them to long and it does indeed work.

I'd already started testing your code and was going to tell you about the problem, before you edited post #13 to ask the question.

Good to hear that you've got it working.

Allows the motor off-time to be adjusted. Should be fairly accurate up to 1/2 the pulse width (30mS).

/**
 **    3state.ino    --    generate 3-state pulse sequence on 2 lines
 **                        set motor off time 
 **                        (over 1/2 pulse width gets flakey)
 **
 **/


#define phase1            4
#define phase2            7
#define motor_off_pd    10000                   // uS
#define period          (30000 / motor_off_pd)  // 30mS @ motor-off rate


void setup( ) {
    pinMode( phase1, OUTPUT );
    pinMode( phase2, OUTPUT );
}


void loop( ) {
    static long time;
    static long last = 0;

    if( ((time = micros( )) - last) >= motor_off_pd ) {
        last = time;
        doPulse( );
    }

    // other (possibly quick) stuff
}


void doPulse( ) {
    static int state[ ][2] = {{1,0}, {0,1}, {0,0}};
    static int phase = 0;
    static int ticks = 0;

    ticks++;

    if( (phase == 2) && (ticks >= period) ) {
          digitalWrite( phase1, state[phase][0] );
          digitalWrite( phase2, state[phase][1] );
        phase = 0;
    }
    else if( ticks >= period ) {
          digitalWrite( phase1, state[phase][0] );
          digitalWrite( phase2, state[phase][1] );
        ticks = 0;
        phase++;
    }
}

Just a thought @johnerrington , I see you combined state 2 with default. While I can see that does what you want the reason I separated them is because state 2 is part of the desired functionality and default is where it goes if some unexpected behaviour somehow makes the state variable something it should never be. My preference is to keep those things separated. This is in no way essential, I happen to think it's tidier.

Thanks Perry, yes I agree. I’ve added a few digital writes to ensure the motor cant be left with both motor lines high as that could potentially cause overheating.

  case 2:
      if (currentMillis - lastMillis >= pulseLength) {
        digitalWrite(phase1, LOW);
        digitalWrite(phase2, LOW);
        state = 0;  //ready for next step
      }
      break;
    default:
      digitalWrite(phase1, LOW);
      digitalWrite(phase2, LOW);
      stepOn = false;
      state = 0;  //ready for next step
  }