Understanding SoftwareSerial... multiple serial Instances

I’m working towards getting an EC and PH sensor from Atlas Scientific to work via an Arduino. My thought was that I could create two software serial instances and read the values accordingly via the computer serial monitor. After trying unsuccessfully for quite a while I’ve decided I understand very little about how things are working here.

For this reason I’m trying to go back to basics and hooked up a button to an LED. When the button is pressed I’m trying to get that value to be “recorded” in one serial instance “phSerial” and then printed via the computer serial monitor. I figured if I can get this working, it should be all the basics to get the sensors working in conjunction.

I have attached my code below. I’m guessing if you look at my code you’ll be able to quickly see where my understanding of how SoftwareSerial works is off…

Thanks in advance for the help…

-Chase

/////////////////////////////////////////////////////////
const int buttonPin = 6;     // the number of the pushbutton pin
const int ledPin =  13;      // the number of the LED pin
int buttonState = 0;         // variable for reading the pushbutton status
//////////////////////////////////////////////////////////

#include <SoftwareSerial.h>

#define PHrxPin 2
#define PHtxPin 3

//#define ECrxPin 4
//#define ECtxPin 5

// set up new serial objects
SoftwareSerial phSerial (PHrxPin, PHtxPin);
//SoftwareSerial ecSerial (ECrxPin, ECtxPin);

void setup() {

  /////////////////////////////////////////////////////////////////
  // initialize the LED pin as an output:
  pinMode(ledPin, OUTPUT);
  // initialize the pushbutton pin as an input:
  pinMode(buttonPin, INPUT);
  /////////////////////////////////////////////////////////////////

  // Open COMPUTER serial communications and wait for port to open:
  Serial.begin(9600);
  while (!Serial) {
    ;
  }
  // set the data rate for both SoftwareSerial ports and start
  phSerial.begin(9600);
  //ecSerial.begin(9600);

}

void loop() {

  /////////////////////////////////////////////////////////////////
  // read the state of the pushbutton value:
  buttonState = digitalRead(buttonPin);

  // check if the pushbutton is pressed.
  // if it is, the buttonState is HIGH:
  if (buttonState == HIGH) {
    // turn LED on:
    digitalWrite(ledPin, HIGH);
    phSerial.print(1);
  } else {
    // turn LED off:
    digitalWrite(ledPin, LOW);
    phSerial.print(2);
  }
  /////////////////////////////////////////////////////////////////

  // begin listening to phSerial transmission
  phSerial.listen();
  Serial.println("Data From pH Sensor:");
  // While data is coming in, read it and send to COMPUTER serial monitor
  while (phSerial.available() > 0) {
    char phValue = phSerial.read();
    Serial.println(phValue);
  }
  // Print blank line
  Serial.println();

  delay(1000);
}

What I can see of the code looks fairly reasonable, even the lines for SoftwareSerial that are commented out.

You do realize that

After trying unsuccessfully for quite a while

is not very helpful to people that are trying to help you for free.

Another issue is that you are doing

    phSerial.print(1);

whenever the button is in the pressed state [which may be detected thousands of times per second except for the delay(1000) statement] rather than detecting when the button has gone from unpressed to pressed. This is covered in the State Change example. There is no debouncing for the button either, but rather than handling debouncing in software, you may have handled it in the hardware that I cannot see - but I doubt it.

The listen() method is used to switch between multiple SoftwareSerial instances. You do not need to call it if you are reading from the same device. This is why multiple software serial ports can be difficult to use… you can only receive from one of them at a time. Data from all the others will be ignored. You have to structure your program to focus on one device at a time, in a round-robin fashion.

Also, listen() empties the input buffer. So if any characters were received during the 1-second delay, you just threw them away.

You shouldn’t use delay. Take look at the “How to do multiple things at once” et al examples in Useful Links. The “Serial Input Basics” section will help you understand how to receive a response string without using delay.

And you should also be aware that SoftwareSerial is very inefficient, because it disables interrupts for long periods of time. This will interfere with other parts of your sketch, or with other libraries. Since your devices are running at 9600, you could use my NeoSWSerial library. It is much more efficient. Much.

A secondary problem with SoftwareSerial is that it cannot transmit and receive at the same time. So if you send a command to one of the devices, received data will be ignored. NeoSWSerial can TX and RX simultaneously.

Here’s your sketch with a debounced button and response parsing (without delay):

/////////////////////////////////////////////////////////
const int ledPin           = 13;
const int buttonPin        =  6;

bool      buttonState      = false;
bool      lastButtonState  = false;
uint32_t  changedTime;
bool      changing         = false;
const uint32_t BOUNCE_TIME = 50000UL;

char      response[20];
uint8_t   length;

//////////////////////////////////////////////////////////

#include <NeoSWSerial.h>

#define PHrxPin 2
#define PHtxPin 3

#define ECrxPin 4
#define ECtxPin 5

NeoSWSerial phSerial (PHrxPin, PHtxPin);
NeoSWSerial ecSerial (ECrxPin, ECtxPin);

void setup()
{
  pinMode(ledPin, OUTPUT);
  pinMode(buttonPin, INPUT);

  Serial.begin(9600);
  while (!Serial)
    ;

  ecSerial.begin(9600);
  phSerial.begin(9600); // begin also calls listen
}

void loop()
{
  buttonLoop();
  phLoop();
  ecLoop();
}

void buttonLoop()
{
  // Watch for (and debounce) button presses
  if (!changing) {

    // See if it's changing now
    buttonState = digitalRead(buttonPin);

    if (lastButtonState != buttonState) {
      lastButtonState  = buttonState;
      changedTime      = millis();       // save when it changed
      changing         = true;

      digitalWrite( ledPin, buttonState ); // set the LED

      // Do something when the button changed
      if (buttonState) {
        phSerial.print( F("R\r") ); // request a reading
        //phSerial.print( 1 ); ???
      } else {
        //phSerial.print( 2 ); ???
      }
    }

  } else { // changing

    // It might be bouncing for a while, has it been long enough?
    if (micros() - changedTime > BOUNCE_TIME) {
      changing = false; // reset to catch when it changes again
    }
  }

} // buttonLoop


void phLoop()
{
  while (phSerial.available() > 0) {
    char c = phSerial.read();

    if (c == '\r') {
      // That's the end of the sensor response
      response[ length ] = '\0'; // NUL-terminate the C string (not the String class)

      Serial.print( F("Data From pH Sensor:") ); // F macro saves RAM
      Serial.println( response ); // just the characters, really

      //  Is the response a reading value or a *XX response code?
      if (isdigit( response[0] )) {

        // Parse the response into a float value
        float phValue = strtod( response, NULL );

        Serial.print( F("phValue = ") );
        Serial.println( phValue );
        
        // ... do something with ph Value?

      } else if (response[0] == '*') {
        // Handle various response codes here...
      }

      length = 0;        // empty the response string...
      ecSerial.listen(); // ... and switch to the EC sensor for a while
      ecSerial.print( F("R\r") ); // ask for an EC reading

    } else {
      //  Save characters until the CR comes in (make sure there's room)
      if (length < sizeof(response)-1)
        response[ length++ ] = c;
    }
  }

} // phLoop


void ecLoop()
{
  while (ecSerial.available() > 0) {
    char c = ecSerial.read();

    if (c == '\r') {
      // That's the end of the sensor response
      response[ length ] = '\0';

      Serial.print( F("Data From EC Sensor:") );
      Serial.println( response );

      //  Is the response a reading value or a *XX response code?
      if (isdigit( response[0] )) {

        // Parse the response into a float value
        float ecValue = strtod( response, NULL );

        Serial.print( F("EC value = ") );
        Serial.println( ecValue );
      
        // ... do something with EC Value?

      } else if (response[0] == '*') {
        // Handle various response codes here...
      }

      length = 0;        // empty the response string...
      phSerial.listen(); // ... and switch back to the ph sensor

    } else {
      //  Save characters until the CR comes in (make sure there's room)
      if (length < sizeof(response)-1)
        response[ length++ ] = c;
    }
  }

} // ecLoop

When the button is pressed, it requests a pH reading, waits for the response, switches to the EC sensor, requests an EC reading and waits for the response. Then it waits for the button to be pressed again.

If a pH response comes in before the button is pressed, it will switch to the EC sensor etc. as above. I don’t know if that’s possible, but you’d have to guard against that if an unsolicited pH response should be ignored.

Cheers,
/dev

@/dev

Truly incredible response. The amount of detail is exceptionally helpful to someone trying to learn with little programming experience. I've come to find that a lot of code documentation is written for people who already think like a coder. Unfortunately, that part of my brain hasn't been shaped yet so I'm left wondering things like...

"in a code snippet like phsensor.listen().... what is doing the listening? Is it the serial instance of phsensor that's listening, or is it the computer (hardware) instance of serial that's listening on the phsensor serial instance?"

This is a fundamental question about how serial (and SoftwareSerial) works. For me, reading through the documentation takes for granted some foundational knowledge beyond these types of questions. So a gap exists that I often find hard to bridge. Answers like yours bridge the gap wonderfully. So thank you.

I will work through your response (and code) slowly and report back with any questions. I'll also check out the link you sent. Looks really great.

Thanks -Chase

@vaj4088

What I can see of the code looks fairly reasonable, even the lines for SoftwareSerial that are commented out.

You do realize that

"After trying unsuccessfully for quite a while..."

is not very helpful to people that are trying to help you for free.

I appreciate the time you took to respond but please clarify what you meant by the above. Are you requesting that I provide more detail to the previous ways I tried to solve the problem?

If so, my effort was to not provide all the ways I had failed, but instead provide the simplest example that I could not get working. In doing so, I figured I could work my way up to the more complicated sensor example without leaning too heavily on "free help."

If you feel it necessary, please respond with how you would prefer I request help in the future.

Respectfully,

-Chase

One of the ways in which this forum works best is when someone properly posts their code and explains in detail that the code does A when they want it to do B.

Often, they just write “it doesn’t work”, which sounds a lot like “After trying unsuccessfully”.

I am glad that /dev was able to help you. Good Luck!

in a code snippet like phsensor.listen().... what is doing the listening? Is it the serial instance of phsensor

Yes, phSensor is an instance of the NeoSWSerial class (aka a variable of that type), and that instance will be the only instance that receives characters. All other instances (e.g., ecSensor) will be "deaf".

or is it the computer (hardware) instance of serial that's listening on the phsensor serial instance?

No, there is no "computer (hardware) instance of serial that's listening on the phsensor serial instance".

The Serial variable is an instance of the HardwareSerial class, and it uses special hardware (a UART) to read or write a byte as a serial sequence of bits. The UART does all the shifting and timing. The software just provides bytes to the UART.

A software serial port, like SoftwareSerial or NeoSWSerial, must read or write each individual bit of each byte, with the correct shifting and timing (aka "bit banging"). This takes a lot more of the CPU's time.

SoftwareSerial is especially bad, because it disables interrupts while it is reading or writing each byte. At 9600, interrupts are disabled for 1ms for each character. A 16MHz Arduino could execute about 10,000 instructions during that time. Instead, the SoftwareSerial code twiddles its thumbs while the byte is received or transmitted, and that prevents anything else from happening.

phSensor.listen() simply selects the instance that will handle the interrupts for each bit being received. Other instances will not handle interrupts for their RX pin, so they are essentially ignoring everything. When transmitting, interrupts are disabled to guarantee the bit timings.

Cheers, /dev

/dev: phSensor.listen() simply selects the instance that will handle the interrupts for each bit being received. Other instances will not handle interrupts for their RX pin, so they are essentially ignoring everything. When transmitting, interrupts are disabled to guarantee the bit timings.

Thanks again for the detailed and thorough response. This last paragraph really cleared things up for me.

-Chase