Bottleneck in ADC to Serial communication rate

Hi everyone,

I'm working on a load cell project which requires high sampling rate and resolution. The aim is to read four load cells continuously as close to 1000Hz as possible using a single ADC. The data is being sent through the Serial for "real-time" plotting in a GUI.

So far, after research and experimentation, I have wired up a 32-bit ADC (TI ADS1262) breakout board to an Arduino UNO and four load cells. I'm able to read all four channels continuously as intended however theres a huge bottleneck in the data conversion from the SPI and only getting ~30Hz data rate on the serial port. I was hoping I could get some advice on how to improve this.

To help my case, I've summarised some of the relevant information:

  • ADS1262 datasheet: https://www.ti.com/lit/ds/symlink/ads1262.pdf

  • I am using the Protocentral library as reference, specifically this example: ProtoCentral_ads1262/1-Simple_Differential.ino at master · Protocentral/ProtoCentral_ads1262 · GitHub. The only real modification was cycling through the ADC channels within the loop. I also changed the .cpp file for the ADC settings I wanted.

  • The load cells are 500kg rated each, with 2mV/V sensitivity. Using a 5V supply from the Arduino and the PGA set at the maximum 32V/V gain, this means only 320mV of useful range so the resolution even with 32-bits is questionable and I will need to investigate further.

  • The ADC data rate is at 19.2kSPS, and the SPI settings are 4MHz (DIV4). The baud rate is at the 115200 maximum (haven't explored putty or anything yet).

Each loop in the arduino script is taking ~35ms to complete - that is 8ms per channel reading and another 2ms in printing values to the serial. Considering I want 1000Hz, this should be 1ms per loop, and the 35 factor reduction in computational time seems a bit scary to me!

I'm an aerospace engineering student so slowly learning this field and don't have sufficient C++ or SPI communication knowledge to understand exactly what is happening.

I've done research and found comparative code for other ADS chips, as well as issues relating to serial port baud rates, Serial.print() vs Serial.write() etc...

From what I've read, I think 1kHz should be very achievable and evidently the bottleneck is in the data conversion. I've attempted to dump the ADC byte readings onto the Serial but wasn't successful.

Again, unsure if this will be a sufficient improvement. Hoping someone might be able to guide me in the right direction with this :slight_smile:

Here is the .ino code:

#include <SPI.h>
#include <SoftwareSerial.h>
#include <math.h>
#include <ads1262.h>

#define PGA 32          // Programmable Gain = 32
#define VREF 2.50       // Internal reference of 2.5V
#define VFSR VREF/PGA

ads1262 ADS; // ADS class

float force;
float volt_V;
float val1;
float val2;
float val3;
float val4;
volatile int i;
volatile char SPI_RX_Buff[10];
volatile long ads1262_rx_Data[10];
volatile static int SPI_RX_Buff_Count = 0;
volatile char *SPI_RX_Buff_Ptr;
volatile int Responsebyte = false;
volatile signed long sads1262Count = 0;
volatile signed long uads1262Count = 0;
double resolution = (double)((double)VREF / pow(2, 31));    // resolution = Vref/(2^n-1) , Vref = 2.5, n = no of bits


void setup()
{
    // initalize the  data ready and chip select pins:
    pinMode(ADS1262_DRDY_PIN, INPUT);   // data ready input line
    pinMode(ADS1262_CS_PIN, OUTPUT);    // chip enable output line
    pinMode(ADS1262_START_PIN, OUTPUT); // start
    pinMode(ADS1262_PWDN_PIN, OUTPUT);  // Power down output

    Serial.begin(115200);      // Max Baud Rate
    ADS.ads1262_Init();        // Initialise ads1262
}

void loop()
{
  // Cycle through differential channels
  val1 = read_channel(1);
  val2 = read_channel(2);
  val3 = read_channel(3);
  val4 = read_channel(4);

  // Print to serial
  Serial.print(val1, 2);
  Serial.print(",");
  Serial.print(val2, 2);
  Serial.print(",");
  Serial.print(val3, 2);
  Serial.print(",");
  Serial.println(val4, 2);
}

// Read and convert adc binary data
float read_channel(int channel) // float
{
    // For changing channels
    switch (channel)
    {
      case 1: // Channel AN0/AN1
          ADS.ads1262_Reg_Write(INPMUX, 0x01);
          break;
      case 2: // Channel AN2/AN3
          ADS.ads1262_Reg_Write(INPMUX, 0x23);
          break;
      case 3: // Channel AN4/AN5
          ADS.ads1262_Reg_Write(INPMUX, 0x45);
          break;
      case 4: // Channel AN6/AN7
          ADS.ads1262_Reg_Write(INPMUX, 0x67);
          break;
    }
    while ((digitalRead(ADS1262_DRDY_PIN)) != LOW)
    {
      continue;
    }

    if ((digitalRead(ADS1262_DRDY_PIN)) == LOW)
    {
    SPI_RX_Buff_Ptr = ADS.ads1262_Read_Data(); // read 6 bytes conversion register

    for (i = 0; i < 5; i++) // Extract 4 bytes of data into buffer
    {
      SPI_RX_Buff[SPI_RX_Buff_Count++] = *(SPI_RX_Buff_Ptr + i);
    }

    ads1262_rx_Data[0] = (unsigned char)SPI_RX_Buff[1]; 
    ads1262_rx_Data[1] = (unsigned char)SPI_RX_Buff[2];
    ads1262_rx_Data[2] = (unsigned char)SPI_RX_Buff[3];
    ads1262_rx_Data[3] = (unsigned char)SPI_RX_Buff[4];

    // Get the raw 32-bit adc count out by shifting
    uads1262Count = (signed long)(((unsigned long)ads1262_rx_Data[0] << 24) | ((unsigned long)ads1262_rx_Data[1] << 16) | (ads1262_rx_Data[2] << 8) | ads1262_rx_Data[3]);
    volt_V = (resolution) * (float)uads1262Count;        // voltage = resolution * adc count (V)
    force = mapfloat(volt_V, 0.00, 0.32, 0.00, 500.00);  // Map voltage to force (kg)
    }
    
    SPI_RX_Buff_Count = 0;
    return force;
}


// Map values in float format
float mapfloat(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;
}

Welcome to the forum. But first, please edit your post to add code tags, as described in the how to use the forum post.

1 Like

What breakout board are you using? It seems extremely unlikely that anything less than a totally professional PCB design, along with extreme attention to grounding, probe leads and shielding, will allow you to get anywhere near 32 bits of resolution. 24 bits is hard enough.

That is a very complex chip, with a similarly complex data sheet, and it is difficult to find answers to simple questions like "how long does it take to switch channels?".

Along with your careful study of the data sheet, you can answer questions like that experimentally, by using micros() to time actions and wait loops like this one:

   unsigned long start = micros();
... //switch channels
    while ((digitalRead(ADS1262_DRDY_PIN)) != LOW)
    {
      continue;
    }
   unsigned long channel_switch_time = micros()-start;

Sprinkle those tests around the code to figure out where the time is going.

Hi jremington,

Thank you for the response. I am using a Protocentral one: ProtoCentral ADS1262 32-bit precision ADC breakout board – Protocentral Electronics

I have no doubts that obtaining proper resolution will be a huge challenge - my current breadboard setup is not helping with noise reduction either!

In terms of channel switching latency... this is the best I have found in the datasheet:

"A higher order sinc filter increases the rejection of the 50-Hz and 60-Hz line cycles, but also increases the filter latency. Filter latency is an important consideration when multiplexing (scanning) through input channels. To make sure that conversions are settled after changing channels, start a new conversion for each channel using the START pin or start command. Note if the multiplexer is changed during ongoing conversions, the conversion is stopped and restarted at the time multiplexer register is changed."

I have done a bit time sampling in the loop but using millis() – I will get back with a more delayed breakdown using the micros() function.

Update:
The bottleneck is in the Reg_Write command to change the multiplexer settings within the switch command, as there are x4 delay(2) calls embedded for cycling the CS line LOW to HIGH to LOW, and back to HIGH after bytes transfer. If I can remove these without affecting the ADC sequence that is 32ms saved.

The Serial.print() commands are still consuming 1.5ms of time on each loop. Would it be worthwhile changing to Serial.write() and outputting the raw data bytes, handling conversion to force readings later down the pipeline?

Alternatively, is there a better way to handle printing to the serial port?

At 115,200 baud on the Serial interface, 1000 samples/seconds will allow for 115.2 data bits per sample. Each byte sent on Serial takes 10 bits (start bit, 8 data bits, stop bit, no parity bit), so you only have time to send 11 bytes per sample. Increase the baud rate to as high as you can reliably use. Sending raw binary instead of ASCII would definitely be better.

If you truly need the 32 bit accuracy from the ADC, do not store the readings in a float, send the raw data from the ADC instead. Float on an UNO is stored as 32-bits, but is limited to 6 to 7 significant digits (note that double is the same as float on an UNO), and your print statements are only printing two decimal places, likely losing much of the resolution of the ADC. Floats are also particularly slow on an UNO, being implemented in software since there is no floating point hardware on the atmega328 chip.

Hi david_2018,

This was extremely useful thank you! I've managed 290Hz at the moment after deleting some delays. Next comes optimising the code I'm working with so will definitely attempt to strip it down to writing binary on the serial.

Currently exploring options for increasing baud rate on the serial. If I'm not mistaken, the Arduino Uno should handle 250-500kbps error free no problem with the 16MHz crystal?

Good to know. I'm pretty sure delays of that magnitude are not needed (or any delays at all), but take a look at the ADC data sheet, which gives the timing requirements for SPI bus communicatons.

There are a couple of Arduinos and imitators like the Teensy 3.2 featuring a USB serial port emulator running at full native USB speed, so the serial Baud rate problem goes away. However, you are left with the 1 ms latency due to the PC scanning the USB port.

Agree with david_2018, don't Serial.print floats, send the raw ADC readings instead.

my current breadboard setup

Ditch that, a breadboard and its wiring are an antenna. Everything will need to be in a grounded, shielded enclosure to get even 24 bits of precision.

1 Like