High Latency on Serial Communication

Hi All, we’re using an Arduino Pro (328 5V) to read from a number of sensors, send to & read a payload from serial & control 3 actuators.

The sensors being read from are 5 analog stretch sensors and 2 BNO55 adafruit IMUs.

The issue we’re having is that despite achieving about 40Hz message frequency there’s a noticable latency of about 200ms between sensor input and data arriving over serial. Ideally this would be about 20ms tops.

I’ve included the arduino code below:

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BNO055.h>
#include <utility/imumaths.h>

//********* SERIAL Communication ***********
// Constants for receiving data from Serial
bool received = false;
byte startByte = 0x10;

const byte payloadOutSize = 54;

struct PayloadOut
{
	float w, x, y, z;
	float w2, x2, y2, z2;
	float ax, ay, az;
	uint16_t thumb, index, middle, ring, pinky;
}payloadOut;

const byte payloadInSize = 9;

struct PayloadIn
{
	uint8_t thumbEffect, thumbAmp, thumbFreq;
	uint8_t indexEffect, indexAmp, indexFreq;
	uint8_t middleEffect, middleAmp, middleFreq;
}payloadIn;

unsigned long counter = 0;
unsigned long timeoutCounter = 0;
int timeoutInterval = 1000;
bool timeout = false;

// Constants for parsing received characters for 
const int numberOfFingers = 3; // Number of Fingers used in the sketch
int solenoidPin[numberOfFingers];
const int numberOfVariables = 9; // Number of variables in the received String
int Signal[numberOfVariables];

// Auxillary variables for pulse generation
unsigned long previousMicros[numberOfFingers] = { 0 };      // For frequency 
long previousMillis[numberOfFingers] = { 0 }; // For pushing 
bool trigger[numberOfFingers] = { false };
int timerVariable = 1;
int lowerBound = 0;

// Auxillary variables for keeping data on fingers in memory
int EffectNumber[numberOfFingers] = { 0 };
int EffectFrequency[numberOfFingers] = { 0 };
int EffectAmplitude[numberOfFingers] = { 0 };

//Debug
int SoftwareSerialRx = 9;
int SoftwareSerialTx = 10;

// Indicator Outputs:
const int ledOutput1 = 7;
const int ledOutput2 = 8;

// Sensors:
const int numberOfSensors = 6;
int inSignal[numberOfSensors]; 
const int AverageN = 15;
int runningAverage[numberOfSensors][AverageN] = {0};
int sensorValueAvg[numberOfSensors] = { 0 };

// IMU:
bool usingIMU = true;  // if we want to have the imu turned on from the very beginning
Adafruit_BNO055 bno1 = Adafruit_BNO055(55);
Adafruit_BNO055 bno2 = Adafruit_BNO055(55, 0x29);
float wOrient, xOrient, yOrient, zOrient;

void setup() {
  // Serial comms start
  Serial.begin(57600); // 38400 or 57600
  Serial.println("Arduino is ready");
  
  pinMode(ledOutput1, OUTPUT);
  pinMode(ledOutput2, OUTPUT);

  // Pins controlling fingers and initiate them
  solenoidPin[0] = 3;
  solenoidPin[1] = 5;
  solenoidPin[2] = 6;
  for (int pin = 0; pin < numberOfFingers; pin++) {
    pinMode(solenoidPin[pin], OUTPUT);
    digitalWrite(solenoidPin[pin], LOW);
  }
  // Initiate sensor pins
  inSignal[0] = A3; // Thumb
  inSignal[1] = A6; // index finger
  inSignal[2] = A7; // Middle finger
  inSignal[3] = A2; // Ring finger
  inSignal[4] = A1; // Little finger
  inSignal[5] = A0; // Extra sensor

  for (int pin1 = 0; pin1 < numberOfSensors; pin1++) {
    pinMode(inSignal[pin1],INPUT);
  }

  // Initializing BNOs
  if (!bno1.begin())
  {
	  // There was a problem detecting the BNO055 ... check your connections
	  Serial.print("Ooops, no BNO055-1 detected ... Check your wiring or I2C ADDR!");
	  usingIMU = false; // revert back to no IMU mode.
						// while(1);
						// break;
  }

  if (!bno2.begin())
  {
	  // There was a problem detecting the BNO055 ... check your connections
	  Serial.print("Ooops, no BNO055-2 detected ... Check your wiring or I2C ADDR!");
	  usingIMU = false; // revert back to no IMU mode.
						// while(1);
						// break;
  }

  delay(500);
  bno1.setExtCrystalUse(true);
  bno2.setExtCrystalUse(true);
}

void loop() {
	// Timeout function	
	if (millis() - timeoutCounter > timeoutInterval) {
		timeout = true;
	}
	else timeout = false;
	// Receive payload
	received = SerialPayloadReceive();

	if (received) {
		FingerLoad();
		Sensing();
		SerialPayloadSend();
		counter++;
		received = false;
		timeoutCounter = millis();
	}
	// FINGER SHIT
	if (timeout) {
		trigger[0] = trigger[1] = trigger[2] = true;
		Signal[0] = Signal[3] = Signal[6] = 0;
	}
	for (int i = 0; i < 3; i++)
	{
		OutputElectricalImpulse(i);
	}
}

void FingerLoad() {
	trigger[0] = payloadIn.thumbEffect;
	trigger[1] = payloadIn.indexEffect;
	trigger[2] = payloadIn.middleEffect;
	Signal[0] = payloadIn.thumbEffect;
	Signal[2] = payloadIn.thumbAmp;
	Signal[1] = payloadIn.thumbFreq;
	Signal[3] = payloadIn.indexEffect;
	Signal[5] = payloadIn.indexAmp;
	Signal[4] = payloadIn.indexFreq;
	Signal[6] = payloadIn.middleEffect;
	Signal[8] = payloadIn.middleAmp;
	Signal[7] = payloadIn.middleFreq;
}

// Send signals to all fingers!
void OutputElectricalImpulse(int pin) {
  float output;
  // Put new data into actuator effect arrays
  if (trigger[pin] == true) {
    if (EffectNumber[pin] != Signal[pin*3] ||
        EffectFrequency[pin] != Signal[pin*3 + 1] || 
        EffectAmplitude[pin] != Signal[pin*3 + 2]) {
      EffectNumber[pin] = Signal[pin*3];
      EffectFrequency[pin] = Signal[pin*3 + 1];
      EffectAmplitude[pin] = Signal[pin*3 + 2];
      digitalWrite(ledOutput1, HIGH);
    }
  }
// DO STUFF HERE
}

// Sensing flex-sensors
void Sensing() {
	for (int i = 0; i < numberOfSensors - 1; i++){
		sensorValueAvg[i] = RunningAverage(i, analogRead(inSignal[i]));
	}
}

int RunningAverage(int sensor, int sensorValue){
	int i = counter % AverageN;
	runningAverage[sensor][i] = sensorValue;
	int average = 0;
	if ( counter < AverageN ){
		for (int j = 0; j < i; j++){
			average += runningAverage[sensor][j];
		}
		return average / counter;
	}
	else {
		for (int j = 0; j < AverageN; j++){
			average += runningAverage[sensor][j];
		}
		return average / AverageN;
	}
}

float reMap(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 SerialPayloadSend() {
	payloadOut.thumb = (uint16_t)sensorValueAvg[0];
	payloadOut.index = (uint16_t)sensorValueAvg[1];
	payloadOut.middle = (uint16_t)sensorValueAvg[2];
	payloadOut.ring = (uint16_t)sensorValueAvg[3];
	payloadOut.pinky = (uint16_t)sensorValueAvg[4];

	/* DIRECT SIGNAL
	payloadOut.thumb = (uint16_t)analogRead(inSignal[0]);
	payloadOut.index = (uint16_t)analogRead(inSignal[1]);
	payloadOut.middle = (uint16_t)analogRead(inSignal[2]);
	payloadOut.ring = (uint16_t)analogRead(inSignal[3]);
	payloadOut.pinky = (uint16_t)analogRead(inSignal[4]);
	*/ 

	if (usingIMU) {
		imu::Quaternion quat = bno1.getQuat();
		payloadOut.w = quat.w();
		payloadOut.x = quat.x();
		payloadOut.y = quat.y();
		payloadOut.z = quat.z();
		quat = bno2.getQuat();
		payloadOut.w2 = quat.w();
		payloadOut.x2 = quat.x();
		payloadOut.y2 = quat.y();
		payloadOut.z2 = quat.z();
		imu::Vector<3> accel = bno1.getVector(Adafruit_BNO055::VECTOR_ACCELEROMETER);
		payloadOut.ax = accel.x();
		payloadOut.ay = accel.y();
		payloadOut.az = accel.z();
	}

	byte outputBuffer[payloadOutSize];
	memcpy(outputBuffer, &payloadOut, payloadOutSize);
	Serial.write(startByte);
	Serial.write(payloadOutSize);
	Serial.write(outputBuffer, payloadOutSize);
}

bool SerialPayloadReceive() {
	if (Serial.available()) {
		byte inbuf[payloadInSize];
		int i = 0;
		// Hang whilst waiting for start byte
		byte inByte;
		while (inByte != startByte) inByte = Serial.read();
		// Read serial into buffer
		while (i < payloadInSize)
		{
			if (Serial.available()) {
				inbuf[i] = Serial.read();
				i++;
			}
		}
		memcpy(&payloadIn, inbuf, payloadInSize);
		return true;
	}
	else return false;
}

Apologies for length! This is read into a custom serial program written in .NET C# using multithreading and the System.IO.Ports library; I can post the code for the serial protocol from that here if that might be the cause of the issue.

As you can see there are a number of hanging functions, the possibility of timeout is dealt with PC-side.

Many thanks!

	byte outputBuffer[payloadOutSize];
	memcpy(outputBuffer, &payloadOut, payloadOutSize);

Why do you need to copy the data from one memory location to another, before sending the data?

Serial data arrives slowly. If there is a single byte, of the 54 (or is that 9?) being sent, in the input buffer, your code blocks, waiting for the rest of the data to come trickling in. That is not necessary. Read and store data as soon as it arrives, regardless of how much that is. When you have a complete packet, then you can deal with it.

Why are you not using a faster baud rate? 115200 is a standard rate, but the Arduino can handle much higher speeds. I'm sure your PC can, too.

The issue we're having is that despite achieving about 40Hz message frequency there's a noticable latency of about 200ms between sensor input and data arriving over serial.

During that time, you have to read 8 analog pins, send 54 bytes of data at a snail's pace, process them on the PC, send 9 bytes back, and receive 9 bytes of data at a snail's pace.

You need to use millis() or micros(), to time each part of the collection, sending, and retrieving process, to see where the time is being wasted.

This does not seem to me to represent the mind-set needed for a responsive system (from code in Original Post)

// Hang whilst waiting for start byte

Have a look at the examples in Serial Input Basics - simple reliable ways to receive data. They all receive data asynchronously without blocking.

...R

Thanks for your prompt replies chaps, Paul - the block of code for copying teh payload into a buffer was to transmit it as a byte stream. Is there a more efficient manner to do this?

Ie, can I pass Serial.write() a pointer to the start of the payload along with it's size and have it written directly?

byte outputBuffer[payloadOutSize];
 memcpy(outputBuffer, &payloadOut, payloadOutSize);
 Serial.write(startByte);
 Serial.write(payloadOutSize);
 Serial.write(outputBuffer, payloadOutSize);

I realise the above is clunky, I wasn't aware of a better manner to do so.

I'm currently benchmarking based on your above recommendations - thanks! As a further question, is there a better manner of creating direct PC/arduino comms then? I have access to some NRF24 modules and receivers, currently we're using an HC05 for bluetooth which does seem to introduce some further overheads.

How are you measuring the latency? Is the PC somehow causing the sensors to move and then you time how long it takes for data to arrive?

ohceejay:
As a further question, is there a better manner of creating direct PC/arduino comms then? I have access to some NRF24 modules and receivers, currently we’re using an HC05 for bluetooth which does seem to introduce some further overheads.

I doubt if the difference in performance would be noticeable, and, if it is otherwise suitable, Bluetooth is probably easier.

…R
Simple nRF24L01+ Tutorial

How are you measuring the latency? Is the PC somehow causing the sensors to move and then you time how long it takes for data to arrive?

I guess the OP is measuring the time from the command on the serial interface sent on the PC to when the complete set of data has arrived on the PC.

@OP: If you’re seeing a 200ms delay with this code I would guess an error in the PC software. You need about 12ms for the serial communication, a few milliseconds are for the calculations and the sensor readings, so 20ms may make sense, 200ms must be outside the Arduuino.

As a further question, is there a better manner of creating direct PC/arduino comms then? I have access to some NRF24 modules and receivers, currently we’re using an HC05 for bluetooth which does seem to introduce some further overheads.

That’s an important information, you have a bluetooth part in your communication stream. That inserts some additional overhead and depending on the PC side that driver may wait for some time before it sends a bluetooth message as there may be more characters to send and it tries to minimize the number of messages to send to save energy.

Try to measure the delay without the bluetooth in the communication chain and post your results.

Dear all, thank you for your various contributions - they were most helpful and I now have asynchronous communication across 4 devices, a hige improvement on the benchmarking above!

Here is the sample code I used for my serial read in the end, I hope it helps anyone with similar questions in the future:

//********* SERIAL Communication ***********
// Constants for receiving data from Serial
bool received = false, receivingSerial = false;
byte startByte = 0x10;
int serialCount = 0;

const byte payloadInSize = 9;

struct PayloadIn
{
 uint8_t thumbEffect, thumbAmp, thumbFreq;
 uint8_t indexEffect, indexAmp, indexFreq;
 uint8_t middleEffect, middleAmp, middleFreq;
}payloadIn;

byte inbuf[payloadInSize];

void setup() {
  // Serial comms start
  Serial.begin(57600); // 38400 or 57600
  Serial.println("Arduino is ready");
}

void loop() { 
 received = SerialPayloadReceive();

 if (received) {
             //DO STUFF
        }
        // DO OTHER STUFF
}

bool SerialPayloadReceive() {
 if (Serial.available()) {
 // check for start byte
 byte inByte;
 while (Serial.available() && !receivingSerial) {
 inByte = Serial.read();
 if (inByte == startByte) {
 receivingSerial = true;
 return false;
 }
 }

 // read into buffer
 if (receivingSerial) {
 while (Serial.available() && serialCount < payloadInSize) {
 inbuf[serialCount] = Serial.read();
 serialCount++;
 }
 }
 // if finished, read into payload and reset
 if (serialCount == payloadInSize) {
 memcpy(&payloadIn, inbuf, payloadInSize);
 serialCount = 0;
 receivingSerial = false;
 return true;
 }
 }
 return false;
}

This is a non-blocking serial function that allows the arduino to read from the serial asynchronously. when receivingSerial is false it let’s the host PC know to send another payload. Performance has dramatically improved - any further comments would be appreciated

Paul - the block of code for copying teh payload into a buffer was to transmit it as a byte stream. Is there a more efficient manner to do this?

The payload that you copied was already a byte array. There was no need to make a copy.

What you are doing is like using a copier/fax machine. First, you make a copy of the page to be faxed, and then you fax the copy. Why?

Fax the original. The process of faxing it will not alter the original in any way.

Ie, can I pass Serial.write() a pointer to the start of the payload along with it's size and have it written directly?

Well, of course you can. That was my point.

The payload is currently a struct, we've identical structs at both ends of the serial with various different datatypes (floats, uint16_t etc) which we copy the byte array into once it is fully arrived. We find this useful for managing the data being transmitted as all we have to do is change the struct at both ends; now the data flow is totally asynchronous this is all the more important. It allows the buffer byte array to fill before we copy it into the payload struct to play with :slight_smile:

ohceejay:
It allows the buffer byte array to fill before we copy it into the payload struct to play with :slight_smile:

Maybe you should use a UNION so that the byte array and the struct are actually the same thing?

...R

Robin2:
Maybe you should use a UNION so that the byte array and the struct are actually the same thing?

This is a great idea, for our purposes it's actually quite helpful that the payload is only updated once all of the data has trickled in over serial. We use that payload every loop to trigger the actuators and other electrical outputs, however the above method would potentially be more efficient

Ta! O

ohceejay:
This is a great idea,

And now that I have got the idea of a Union into your head you don't really need to create a union to get the desired effect. Just treat the name of the struct as a pointer (address) to its first byte and write the incoming data to each successive byte.

...R