IRQ based Software Serial, read function only

One of the problems often mentioned with the UNO is the number of (HW) serial ports.

There are some SW Serial implementations and one of the heard complaints is that they are blocking.

Last weekend I explored the idea to use the hardware interrupt on the UNO as a Serial in line (RX) so I could have 2 serial reads on my UNO. In theory this could be connected to the PinChangeInterrupt but I did not try that.

The working is as follows. A bytes sent over serial is preceded by a start-bit and followed by a stop-bit. 10 bits to form a byte. The only 2 fixed transitions is a HIGH for the start bit and LOW for the stop bit. For the rest the bits can change depending on the byte send. Every CHANGE is triggering an interrupt.

The ISR detects the start of the bit stream and initializes variables used. Every next CHANGE LOW/HIGH is used to decode one or more bits. This is done by tracking the micros() since last transition as that indicates the # bits HIGH or LOW. If the baud rate is known the time per bit is known. E.g. if a bit takes 100 usec then a time of 300usec since last transition means 3 identical bits.

As it is interrupt based the reading does run “in the background” which consumes some time but does not block the main loop as some SWSerial implementations do.

The code is still very experimental but behaves stable at 9600 and 19200 baud on a UNO. 38400 fails too often and higher speeds are even worse. The code is not tested on a MEGA or DUE (might need tweaking) but should be easy to port.

I tested with Serial Monitor and PUTTY.EXE on win7/64. On the UNO board I connected the RX pin with pin2 == IRQ0. That’s all tests done, so use it carefully.

//
//    FILE: SWserialIRQ.ino
//  AUTHOR: Rob Tillaart
// VERSION: 0.1.04
// PURPOSE: get serial-in on interrupt pin (experimental)
//    DATE: 2015-11-08
//     URL: http://forum.arduino.cc/index.php?topic=358465
//
// Released to the public domain
//

// results of first tests.
// works stable for 9600, 14400, 19200
// works good for 28800, but incidental an error.
// fails too often for 38400

//#define BRATE   9600
//#define BR_BIT  100
//#define BRATE   14400
//#define BR_BIT  64
//#define BRATE   28800
//#define BR_BIT  32
//#define BRATE   38400
//#define BR_BIT  24
#define BRATE   19200
#define BR_BIT  48
#define BR_BYTE (10*BR_BIT)

volatile byte buffer[32];
volatile byte head = 0;
volatile byte tail = 0;

volatile uint16_t startTime;
volatile uint16_t lastTime;
volatile uint16_t b = 0;
volatile uint16_t mask = 0;
volatile uint8_t f = 0;
volatile uint8_t bits = 0;

void pin_1_isr()
{
  uint16_t time = micros();
  // start bit?
  if (time - startTime > BR_BYTE)
  {
    startTime = time;
    lastTime = time;
    b = 0;
    f = 0;
    mask = 0x0001;
    bits = 1;
    return;
  }

  // process bits
  while (time - lastTime >= BR_BIT)
  {
    lastTime += BR_BIT;
    if (f == 1) b |= mask;
    mask <<= 1;
    bits++;
  }
  f = !f;
  lastTime = time;

  // found the stop bit?
  if (bits >= 9)
  {
    buffer[head] = b >> 1; // remove start bit
    head++;
    if (head == 32) head = 0;
    bits = 0;
    return;
  }
}

void setup()
{
  Serial.begin(BRATE);
  Serial.print("Start ");
  Serial.println(__FILE__);
  attachInterrupt(0, pin_1_isr, CHANGE);
}

void loop()
{
  if (tail != head)
  {
    //    Serial.print("0x");
    //    Serial.print(buffer[tail], HEX);
    //    Serial.print("\t");
    //    Serial.println((char)buffer[tail]);
    Serial.print((char)buffer[tail]);
    tail++;
    if (tail == 32) tail = 0;
  }
}

Can be used for reading a GPS or Sensor (e.g. cozir CO2) that constantly sends data @9600 baud.

As always remarks, comments and improvements are welcome.

Are you aware of my much earlier Thread with the same title - and (I think) the same purpose.

...R

Missed that one ... some reading to do

thanks

robtillaart:
Missed that one ... some reading to do

Perhaps you would consider changing the title of this Thread to avoid search confusion ?

Thanks

...R

…and there’s some more reading, if you like, in the 3 serial classes I just posted:

NeoSWSerial
NeoICSerial
NeoHWSerial

They’re all C++ classes. The first one is based on Robin’s earlier work, the second is based on PaulS’s AltSoftSerial, and the last is based on HardwareSerial.

Cheers,
/dev

Robin2:
Perhaps you would consider changing the title of this Thread to avoid search confusion ?

Thanks

...R

Done...

/dev:
…and there’s some more reading, if you like, in the 3 serial classes I just posted:

NeoSWSerial
NeoICSerial
NeoHWSerial

They’re all C++ classes. The first one is based on Robin’s earlier work, the second is based on PaulS’s AltSoftSerial, and the last is based on HardwareSerial.

Cheers,
/dev

Indeed more reading, thanks!

@/dev
The NEOSWlibrary is very much alike in essence, main difference is that you use a HW timer where I use micros().
And your code is far more complete , well done!

you use a HW timer where I use micros()

For these small time intervals, the rollover in an 8-bit value doesn't matter, and speed is somewhat of the essence in this context. I have yet to review all the MCUs to make sure that right timer register is selected, and that it's running at 8MHz by default.

well done!

Thanks! I think there's a few things to tidy up from extending jboyton's gSoftSerial to support multiple instances... I haven't tested that very extensively, yet. I'm also investigating ways to support binary, not just ASCII 0x00-0x7F. He counts on the MSB being 0 to terminate each character time. Very efficient, and it works out very well for the ASCII NMEA stream.

Cheers,
/dev

robtillaart:
Done...

Very much appreciated.

I would be interested to know what led you to consider writing your program?

...R

Robin2:
I would be interested to know what led you to consider writing your program?

There was a question on the forum recently - http://forum.arduino.cc/index.php?topic=357879 - from a person that wanted 6 serial ports to read 6 serial devices on an UNO.
I discussed how it could work and during the weekend I was still triggered (challenged) I decided to do some feasibility exploration. That resulted in the above code.

Wrote a simple test sketch to see if it worked for all 256 values possible. It did work on 9600, 19200 and 38400. If the test fails the onboard led will be on, otherwise off. before the test it is 1 second on.
to run the test one must connect TX pin1 with pin2 on the UNO.
Still no real test but feels good sofar.

Need to think about a good test.

//
//    FILE: SWserialIRQTest.ino
//  AUTHOR: Rob Tillaart
// VERSION: 0.1.00
// PURPOSE: get serial-in on interrupt pin (experimental)
//    DATE: 2015-11-10
//     URL: http://forum.arduino.cc/index.php?topic=358465
//
// Released to the public domain
//


//#define BRATE   9600
//#define BR_BIT  100
//#define BRATE   14400
//#define BR_BIT  64
//#define BRATE   28800
//#define BR_BIT  32
#define BRATE   38400
#define BR_BIT  24
//#define BRATE   19200
//#define BR_BIT  48
#define BR_BYTE (10*BR_BIT)

volatile byte buffer[32];
volatile byte head = 0;
volatile byte tail = 0;

volatile uint16_t startTime;
volatile uint16_t lastTime;
volatile uint16_t b = 0;
volatile uint16_t mask = 0;
volatile uint8_t f = 0;
volatile uint8_t bits = 0;

void pin_1_isr()
{
  uint16_t time = micros();
  // start bit?
  if (time - startTime > BR_BYTE)
  {
    startTime = time;
    lastTime = time;
    b = 0;
    f = 0;
    mask = 0x0001;
    bits = 1;
    return;
  }

  // process bits
  while (time - lastTime >= BR_BIT)
  {
    lastTime += BR_BIT;
    if (f == 1) b |= mask;
    mask <<= 1;
    bits++;
  }
  f = !f;
  lastTime = time;

  // found the stop bit?
  if (bits >= 9)
  {
    buffer[head] = b >> 1; // remove start bit
    head++;
    if (head == 32) head = 0;
    bits = 0;
    return;
  }
}

void setup()
{
  Serial.begin(BRATE);
  Serial.print("Start ");
  Serial.println(__FILE__);
  attachInterrupt(0, pin_1_isr, CHANGE);

  pinMode(13, OUTPUT);
  digitalWrite(13, HIGH);
  delay(1000);
  digitalWrite(13, LOW);
  SWSTest();

  Serial.println("Done...");
}

void loop()
{
}

void SWSTest()
{
  for (int c = 0; c < 256; c++)
  {
    Serial.write((byte)c);
    while (tail == head);

    int x = buffer[tail];
    tail++;
    if (tail == 32) tail = 0;

    if (x != c)
    {
      digitalWrite(13, HIGH);
    }
  }
}

update: was a typo in the test above causing it to block, needs a redo.

Your code looks simpler than equivalent part of my Yet Another Software Serial.

It would be interesting to see if there is any difference in the "load" they impose on the Arduino, and to compare them with the standard SoftwareSerial. By "load" I mean how other stuff can the Arduino do while still successfully receiving data.

If it is more effective than SoftwareSerial it would be interesting to see if it could work with pinChange interrupts to give a wider number of inputs - and could it manage two or more at the same time.

I wrote my code so it would not conflict with the Servo library and so that the "how it works" would be more easily available to interested parties.

Alas I have two or three other things I should attend to before I stufy this further. I will bookmark this Thread so I can come back to it.

...R

As my implementation does not use any timer directly it might look as non interfering but it at least takes time to do the things it does.

SW Serial is blocking for the whole 10 bits, at 9600 baud that is approx 1 usec.

The interrupt based code above is called between 4 and 10 times (if counted correctly), depending on the pattern. Every ISR call has at least a call to micros() which takes 3.5 usec (IIRC). The processing of the 10 bits takes I guess max 10 usec per bit, some overhead so in total it will be some 200 usec per byte max.

Lets think of a sketch to measure the real numbers.

test sketch above is not working as intended, need to rethink ==> better use a 2nd UNO :slight_smile: