RobTillaart / ADS1X15

Arduino library for ADS1015 = I2C 12 bit ADC and ADS1115 = I2C 16 bit ADC
MIT License
155 stars 29 forks source link

Synchronous mode very slow #53

Closed git2212 closed 1 year ago

git2212 commented 1 year ago

Hi, Based on reading the docs I set up an ADS1115 with the following:

    _ads=new ADS1115(_addr);
    // initialize the Analog2Digital sampler
    volatile bool status=_ads->begin();
    _ads->setGain(ADS1x15_GAIN_4_096_VOLTS);
    // set the highest datarate, 860 samples/sec
    // see https://github.com/RobTillaart/ADS1X15       
    _ads->setDataRate(7);   
    // single shot mode
    _ads->setMode(1);
    // set the I2C bus frequency
    _ads->setWireClock(400000);

I access the sampler asynchronously using:

unsigned long start=micros();
_ads-> requestADC_Differential_0_1()
while(!_ads->isReady(){
    yield();
}
unsigned long elapsed=micros()-start;
_ads->getValue();

so to be fair, I wasn't expecting to be able to take 860 samples per second this way, but my benchmarking was a bit disappointing... it takes at least 6300 micros (6.3 millis) to get a sample. I could be doing something wrong but it doesn't seem like it...

To satisfy my curiousity, i switched to a synchronous blocking read like so"

unsigned long start=micros();
_ads->readADC_Differential_0_1()
unsigned long elapsed=micros()-start;

synchronous reads take at least 6500 micros (6.5 millis). My I2C clock is anywhere between 300-400kHZ, lots of clock stretching going on so hard to be more precise with that number. Also, if it makes a difference running on an RP2040 pico, this is the only I2C device on the bus...

so by my math, we are looking at 150 samples per sec ... I haven't dug into the data sheet and perhaps differential mode is slower, the performance numbers you list, i.e. 860 s/sec, don't seem to be achievable in my case... suggestions?

RobTillaart commented 1 year ago

Thanks for this issue,

The speed of 860 sps can only be reached in continuous mode and using interrupts to signal when data is ready. See example.

Both sync an async methods loosing quite some time with polling the status. Did not study that in detail.

What might be an option is to combine async requesting a conversion and use interrupts to trigger the reading of the result. Polling a flag is faster than requesting status over I2C.

RobTillaart commented 1 year ago

You could try the async code without the yield() call. Do not know its overhead.

RobTillaart commented 1 year ago

Differential might indeed be slower as one need to make two samples and compare them. You need to check the datasheet what it tells about differential speed, I do not recall it from my head.

RobTillaart commented 1 year ago

Similar / related - https://github.com/RobTillaart/ADS1X15/issues/49

RobTillaart commented 1 year ago

The speed of 860 sps can only be reached in continuous mode and using interrupts to signal when data is ready.

Learned from data sheet today: This is not 100% true, one could also set continuous mode and read the sensor with getValue() as often as possible, no need to wait for an interrupt. However one might read the ADC too often (==>results in same value).

RobTillaart commented 1 year ago

Differential might indeed be slower as one need to make two samples and compare them. You need to check the datasheet what it tells about differential speed, I do not recall it from my head.

From data sheet

image

So reading a differential 0_1 should be as fast as reading a single channel. (not tested with hardware yet)

git2212 commented 1 year ago

Thanks for the quick reply!... BTW, nifty little library you have written :)

Regarding the time cost of yield(), that will depend on a number of runtime factors, in my case, given I am running a 133Mhz processor and not much else is going on while I read the sensor, with or without yield() I get the same results.

So moving along... I've dug through the data sheet (what else would anyone want to do at 2am hahaha) and back to more empirical testing ofcourse... I've switched to continuous mode to figure out the cost of single shot sampling the following code sequence worked:

_ads-> requestADC_Differential_0_1(); // called only once at the beginning to kick things off ;)
.
.
.
unsigned long start=micros();
_ads->getValue();
unsigned long elapsed=micros()-start;

In continuous mode, getting the next sample over I2C takes 250 micros (or 0.25 millis) understanding that the data rate limits set still apply so while the getValue() is quicker, not each value read represents a "new" sample.

Next I looked at benchmarking the single channel mode, there doesn't appear to be any significant difference between differential mode and single channel mode, the getValue() still takes 250 micros.

So, single shot mode appears to have a read penalty at the application level of 6250 micros (or 6.25 millis). I can't tell if the ADS1115 itself is responsible, but on p21 of the spec sheet it says

an ADS111x in power-down state with a data rate set to 860 SPS can be operated by a microcontroller that instructs a single-shot conversion every 125ms (8 SPS). A conversion at 860 SPS only requires approximately 1.2 ms

although it's an assumption that something specific to 'duty cycle' might apply to existing sleep mode... the call to requestADC_Differential_0_1() takes 160(ish) micros the rest comes from waiting. I have not benchmarked each isReady() individually as I would expect reading the register on the ADS1115 would not throw outliers into the mix, well I could be wrong of course on the outliers :) So mileage for others may vary depending on the board and wiring/routing they use but at 400Hz I2C bus speed for my setup, those are the running times.

So there is quite a delta between the claim and the facts on the ground... From what I can tell from your code, there is not much else you could do but read/write the registers, which you do. I2C could be a factor if we are saturating the bus but adding a few micros delay between each isReady() should solve that problem (it doesn't!), lower I2C bus speeds will increase the timings but in a non-linear fashion. I don't have a $10,000 logic analyzer to dig into the bus workings, but I'd say the I2C bus looks good, the clock stretching observed is marginal... the rest who knows.

So is TI misrepresenting single shot mode?... would be something then ... grasping at straws here... it would not be the first time that the $2 board is using a fake chip and is not the genuine TI chip (well that never happens) ...

git2212 commented 1 year ago

Similar / related - #49 Yes similar in the sense that it's about user level performance or lack thereof. However, in my testing redefining ADS1115_CONVERSION_DELAY to a lower value has little impactl.

The OP for #49 has a more severe case theoretically as there 3 inputs are being used. So his issue(s) if any should arise from multiplexing the inputs, all commercial ADCs seem to do that so when all the inputs are used, the theoretical bandwidth is never attainable, well not on these low end ADCs anyway. In differential mode I would have expected them to include circuitry similar to an instrumentation amplifier with good common mode rejection, etc, I guess they are multiplexing the input in differential mode as well.

Be that as it may, my situation and testing on the differential mode is yielding dismal results and certainly not within any reasonable tolerance or expectation vis-a-vis the often touted bandwidth. The spec sheet is unhelpful as it seems to repeatedly refer to its maximum bandwidth without qualifying it properly in context, well at least they are not using dBs like many spec sheets to further hamper most people lol.

In my estimation, the issue is not with your library, it's the chip. I regularly use and buy TI as their quality has been one of the best over the years, so not sure what gives with this ADS1115. The ADS1115 offers a lot of features which under the right circumstances would simplify circuit design so I get where they are going with it, however, the ADS1115s I have, just don't perform anywhere near what they could/should.

As I said before, I also suspect that our el-cheapo boards are not using genuine TI chips... a conversation for another day of course, needless to say "they" copy everything so why not ADS1115... how else could you buy these boards for $2/piece on Aliexpress and resell them on eBay for $15 ;)... it's a wonderful conspiracy theory of course and now I sound like an apologist for TI :))

Some day I may just create a prototype board with an ADS1115 which I know to be genuine TI, however a quick look on digikey reveals that the SPI based ADS7816 would likely be a better performer in a custom implementation.

RobTillaart commented 1 year ago

BTW, nifty little library you have written :)

thanks!

RobTillaart commented 1 year ago

In continuous mode, getting the next sample over I2C takes 250 micros (or 0.25 millis) understanding that the data rate limits set still apply so while the getValue() is quicker, not each value read represents a "new" sample.

Use of the RDY flag with an interrupt may solve this pretty well as the interrupt comes when new data is ready.

SPI based ADS7816 would likely be a better performer in a custom implementation.

ADS7816 is "only" 12 bit - https://www.ti.com/product/ADS7816

Or ADS1118 16 bit - https://www.ti.com/product/ADS1118 Interesting as this ADS1118 data sheet does also describe the ADS1115 so I expect these to be very close related.

ADS1118

image

ADS1115

image

(Sorry, don't have a library for the 1118 )

RobTillaart commented 1 year ago

Note ADS1118 == 3V3 device.

RobTillaart commented 1 year ago

Did some test today

UNO (16 MHz) IDE 1.8.19

Sketch ADS_performance.ino, slightly modified to include I2C speed setting and SPS calculation. (will be included in next release)

//
//    FILE: ADS_performance.ino
//  AUTHOR: Rob.Tillaart
// PURPOSE: read analog input
//     URL: https://github.com/RobTillaart/ADS1X15

// test
// connect 1 potmeter
//
// GND ---[   x   ]------ 5V
//            |
//
// measure at x (connect to AIN0).

#include "ADS1X15.h"

// choose your sensor
// ADS1013 ADS(0x48);
// ADS1014 ADS(0x48);
// ADS1015 ADS(0x48);
// ADS1113 ADS(0x48);
// ADS1114 ADS(0x48);

ADS1115 ADS(0x48);

uint32_t start, d1, d2;
int x;

void setup()
{
  Serial.begin(115200);
  Serial.println(__FILE__);
  Serial.print("ADS1X15_LIB_VERSION: ");
  Serial.println(ADS1X15_LIB_VERSION);

  ADS.begin();

  Wire.setClock(400000);

  ADS.setGain(0);  // 6.144 volt

  for (int dr = 0; dr < 8; dr++)
  {
    //  0 = slow   4 = medium   7 = fast
    ADS.setDataRate(dr);
    Serial.print("DR:\t");
    Serial.println(dr);

    test_single_shot();
    test_continuous();

    Serial.print("\t\tFACTOR:\t");
    Serial.println(1.0 * d1 / d2);
  }

  Serial.println("\nDone...");
}

void loop()
{
}

void test_single_shot()
{
  Serial.print(__FUNCTION__);

  ADS.setMode(1);
  start = micros();
  x = ADS.readADC(0);
  for (int i = 0; i < 100; i++)
  {
    x = ADS.readADC(0);
  }
  d1 = micros() - start;
  Serial.print("\t");
  Serial.print(d1);
  Serial.print("\t\t");
  Serial.println(100000000.0 / d1);
  delay(100);
}

void test_continuous()
{
  Serial.print(__FUNCTION__);

  ADS.setMode(0);
  start = micros();
  x = ADS.readADC(0);
  for (int i = 0; i < 100; i++)
  {
    x = ADS.getValue();
  }
  d2 = micros() - start;
  Serial.print("\t\t");
  Serial.print(d2);
  Serial.print("\t\t");
  Serial.println(100000000.0 / d2);
  delay(100);
}

//  -- END OF FILE --

Put it into a table: Synchronous calls I2C 100 KHz

DataRate Time 100 calls SPS
0 12861340 7.78
1 6481396 15.4
2 3347512 29.9
3 1724380 58.0
4 941032 106
5 549204 182
6 381340 262
7 269448 371

Synchronous calls I2C 400 KHz

DataRate Time 100 calls SPS
0 12872804 7.77
1 6402848 15.6
2 3234156 30.9
3 1649272 60.6
4 862188 116
5 468652 213
6 271552 368
7 173412 577

Synchronous calls I2C 600 KHz

DataRate Time 100 calls SPS
0 12736788 7.85
1 6390104 15.7
2 3223568 31.0
3 1645768 60.8
4 852300 117
5 448520 223
6 261216 383
7 167660 596

These are maxima of the SPS feasible, they do not include further processing. At least this test shows the effect of the I2C bus speed.

RobTillaart commented 1 year ago

If no questions remain, you may close the issue. (you can always reopen if needed or create a new topic.)