From 62b39f5e3e0650611c5e20321f018898e0c8076d Mon Sep 17 00:00:00 2001 From: WizardTim <43670403+WizardTim@users.noreply.github.com> Date: Tue, 1 Dec 2020 16:14:48 +1000 Subject: [PATCH 01/14] More statistics to detect bad/over polling --- examples/ads1x15_fast_read.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/examples/ads1x15_fast_read.py b/examples/ads1x15_fast_read.py index 1e499b8..e585415 100644 --- a/examples/ads1x15_fast_read.py +++ b/examples/ads1x15_fast_read.py @@ -22,6 +22,8 @@ ads.mode = Mode.CONTINUOUS ads.data_rate = RATE +repeats = 0 + data = [None] * SAMPLES start = time.monotonic() @@ -29,9 +31,26 @@ # Read the same channel over and over for i in range(SAMPLES): data[i] = chan0.value + if data[i] == data[i-1]: + repeats += 1 + end = time.monotonic() total_time = end - start -print("Time of capture: {}s".format(total_time)) -print("Sample rate requested={} actual={}".format(RATE, SAMPLES / total_time)) +rate_reported = SAMPLES / total_time +rate_actual = (SAMPLES-repeats) / total_time +# NOTE: cannot detect conversion rates higher than polling rate + +print("Took {:5.3f} s to acquire {:d} samples.".format(total_time, SAMPLES)) +print("") +print("Configured:") +print(" Requested = {:5d} sps".format(RATE)) +print(" Reported = {:5d} sps".format(ads.data_rate)) +print("") +print("Actual:") +print(" Polling Rate = {:8.2f} sps".format(rate_reported)) +print(" {:9.2%}".format(rate_reported / RATE)) +print(" Repeats = {:5d}".format(repeats)) +print(" Conversion Rate = {:8.2f} sps (estimated)".format(rate_actual)) + From 9e7e017014cd50802432f1c850b27acbe31ed899 Mon Sep 17 00:00:00 2001 From: WizardTim <43670403+WizardTim@users.noreply.github.com> Date: Tue, 1 Dec 2020 16:32:00 +1000 Subject: [PATCH 02/14] Expand comments --- examples/ads1x15_fast_read.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/ads1x15_fast_read.py b/examples/ads1x15_fast_read.py index e585415..5e68498 100644 --- a/examples/ads1x15_fast_read.py +++ b/examples/ads1x15_fast_read.py @@ -31,6 +31,7 @@ # Read the same channel over and over for i in range(SAMPLES): data[i] = chan0.value + # Detect repeated values due to over polling if data[i] == data[i-1]: repeats += 1 @@ -40,7 +41,7 @@ rate_reported = SAMPLES / total_time rate_actual = (SAMPLES-repeats) / total_time -# NOTE: cannot detect conversion rates higher than polling rate +# NOTE: leave input floating to pickup some random noise, this cannot estimate conversion rates higher than polling rate print("Took {:5.3f} s to acquire {:d} samples.".format(total_time, SAMPLES)) print("") From 2de02e0e36870bf33c3c892d8d4e50a5a41f50a6 Mon Sep 17 00:00:00 2001 From: WizardTim <43670403+WizardTim@users.noreply.github.com> Date: Wed, 2 Dec 2020 09:57:15 +1000 Subject: [PATCH 03/14] Reformat code with psf/black --- examples/ads1x15_fast_read.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ads1x15_fast_read.py b/examples/ads1x15_fast_read.py index 5e68498..a298aec 100644 --- a/examples/ads1x15_fast_read.py +++ b/examples/ads1x15_fast_read.py @@ -32,7 +32,7 @@ for i in range(SAMPLES): data[i] = chan0.value # Detect repeated values due to over polling - if data[i] == data[i-1]: + if data[i] == data[i - 1]: repeats += 1 @@ -40,7 +40,7 @@ total_time = end - start rate_reported = SAMPLES / total_time -rate_actual = (SAMPLES-repeats) / total_time +rate_actual = (SAMPLES - repeats) / total_time # NOTE: leave input floating to pickup some random noise, this cannot estimate conversion rates higher than polling rate print("Took {:5.3f} s to acquire {:d} samples.".format(total_time, SAMPLES)) From ee9eb68a1012fd44bc84520a4ec4e8b72189f4ea Mon Sep 17 00:00:00 2001 From: WizardTim <43670403+WizardTim@users.noreply.github.com> Date: Wed, 2 Dec 2020 10:04:44 +1000 Subject: [PATCH 04/14] Fix EOF for psf/black --- examples/ads1x15_fast_read.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/ads1x15_fast_read.py b/examples/ads1x15_fast_read.py index a298aec..335c68c 100644 --- a/examples/ads1x15_fast_read.py +++ b/examples/ads1x15_fast_read.py @@ -54,4 +54,3 @@ print(" {:9.2%}".format(rate_reported / RATE)) print(" Repeats = {:5d}".format(repeats)) print(" Conversion Rate = {:8.2f} sps (estimated)".format(rate_actual)) - From 1f94dafcb1880b60b33a16bb859d25b5a259bb39 Mon Sep 17 00:00:00 2001 From: WizardTim <43670403+WizardTim@users.noreply.github.com> Date: Wed, 2 Dec 2020 10:22:23 +1000 Subject: [PATCH 05/14] Wrap long line for psf/black --- examples/ads1x15_fast_read.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/ads1x15_fast_read.py b/examples/ads1x15_fast_read.py index 335c68c..801a2c7 100644 --- a/examples/ads1x15_fast_read.py +++ b/examples/ads1x15_fast_read.py @@ -41,7 +41,8 @@ rate_reported = SAMPLES / total_time rate_actual = (SAMPLES - repeats) / total_time -# NOTE: leave input floating to pickup some random noise, this cannot estimate conversion rates higher than polling rate +# NOTE: leave input floating to pickup some random noise +# This cannot estimate conversion rates higher than polling rate print("Took {:5.3f} s to acquire {:d} samples.".format(total_time, SAMPLES)) print("") From 205146d0ba97aa9e76796ffbc6b82907e89c9df8 Mon Sep 17 00:00:00 2001 From: WizardTim <43670403+WizardTim@users.noreply.github.com> Date: Wed, 2 Dec 2020 12:51:20 +1000 Subject: [PATCH 06/14] Comment conversion result read logic --- adafruit_ads1x15/ads1x15.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/adafruit_ads1x15/ads1x15.py b/adafruit_ads1x15/ads1x15.py index ccb3a88..4c17922 100644 --- a/adafruit_ads1x15/ads1x15.py +++ b/adafruit_ads1x15/ads1x15.py @@ -157,9 +157,14 @@ def _conversion_value(self, raw_adc): def _read(self, pin): """Perform an ADC read. Returns the signed integer result of the read.""" + # Immediately return conversion register result if in CONTINUOUS mode and pin has not changed if self.mode == Mode.CONTINUOUS and self._last_pin_read == pin: return self._conversion_value(self.get_last_result(True)) + + # Assign last pin read if in SINGLE mode or first sample in CONTINUOUS mode on this pin self._last_pin_read = pin + + # Configure ADC every time before a conversion in SINGLE mode or changing channels in CONTINUOUS mode if self.mode == Mode.SINGLE: config = _ADS1X15_CONFIG_OS_SINGLE else: @@ -171,12 +176,15 @@ def _read(self, pin): config |= _ADS1X15_CONFIG_COMP_QUE_DISABLE self._write_register(_ADS1X15_POINTER_CONFIG, config) + # Wait for conversion to complete + # ADS1x1x devices settle within a single conversion cycle if self.mode == Mode.SINGLE: - # poll conversion complete status bit + # Continuously poll conversion complete status bit while not self._conversion_complete(): pass else: - # just sleep (can't poll in continuous) + # Can't poll registers in CONTINUOUS mode + # Wait expected time for two conversions to complete time.sleep(2 / self.data_rate) return self._conversion_value(self.get_last_result(False)) From f28460d51d491fc72948264b0290404049fd5f14 Mon Sep 17 00:00:00 2001 From: WizardTim <43670403+WizardTim@users.noreply.github.com> Date: Wed, 2 Dec 2020 13:03:32 +1000 Subject: [PATCH 07/14] Wait for conversions in continuous example Previously polling rate (apparent sample rate) was limited purely by I2C speed, script will now wait a full conversion cycle until it next reads the conversion result. NOTE: Not implementing a time.sleep() in _read(self, pin) as this would be blocking and would inhibit easy implementation of interrupt driven sampling by the ALERT/RDY pin. --- examples/ads1x15_fast_read.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/examples/ads1x15_fast_read.py b/examples/ads1x15_fast_read.py index 801a2c7..0441620 100644 --- a/examples/ads1x15_fast_read.py +++ b/examples/ads1x15_fast_read.py @@ -10,6 +10,9 @@ SAMPLES = 1000 # Create the I2C bus with a fast frequency +# NOTE: Your device may not respect the frequency setting +# Raspberry Pis must change this in /boot/config.txt + i2c = busio.I2C(board.SCL, board.SDA, frequency=1000000) # Create the ADC object using the I2C bus @@ -26,16 +29,23 @@ data = [None] * SAMPLES +time_last_sample = time.monotonic() start = time.monotonic() # Read the same channel over and over for i in range(SAMPLES): + # Wait for expected conversion finish time + while time.monotonic() < (time_last_sample + (1.0 / ads.data_rate)): + pass + + # Read conversion value for ADC channel + time_last_sample = time.monotonic() data[i] = chan0.value + # Detect repeated values due to over polling if data[i] == data[i - 1]: repeats += 1 - end = time.monotonic() total_time = end - start From c74de29fb3dd28edc7048092cc89a59c6ce78529 Mon Sep 17 00:00:00 2001 From: WizardTim <43670403+WizardTim@users.noreply.github.com> Date: Wed, 2 Dec 2020 16:56:04 +1000 Subject: [PATCH 08/14] Remove whitespace for psf/black --- adafruit_ads1x15/ads1x15.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_ads1x15/ads1x15.py b/adafruit_ads1x15/ads1x15.py index 4c17922..47e9072 100644 --- a/adafruit_ads1x15/ads1x15.py +++ b/adafruit_ads1x15/ads1x15.py @@ -160,10 +160,10 @@ def _read(self, pin): # Immediately return conversion register result if in CONTINUOUS mode and pin has not changed if self.mode == Mode.CONTINUOUS and self._last_pin_read == pin: return self._conversion_value(self.get_last_result(True)) - + # Assign last pin read if in SINGLE mode or first sample in CONTINUOUS mode on this pin self._last_pin_read = pin - + # Configure ADC every time before a conversion in SINGLE mode or changing channels in CONTINUOUS mode if self.mode == Mode.SINGLE: config = _ADS1X15_CONFIG_OS_SINGLE From 45a5775f8b6a2cd3fb7063704a88b3fa058d32b0 Mon Sep 17 00:00:00 2001 From: WizardTim <43670403+WizardTim@users.noreply.github.com> Date: Wed, 2 Dec 2020 17:01:26 +1000 Subject: [PATCH 09/14] Wrap lines for PyLint --- adafruit_ads1x15/ads1x15.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/adafruit_ads1x15/ads1x15.py b/adafruit_ads1x15/ads1x15.py index 47e9072..0ff0e8b 100644 --- a/adafruit_ads1x15/ads1x15.py +++ b/adafruit_ads1x15/ads1x15.py @@ -157,14 +157,16 @@ def _conversion_value(self, raw_adc): def _read(self, pin): """Perform an ADC read. Returns the signed integer result of the read.""" - # Immediately return conversion register result if in CONTINUOUS mode and pin has not changed + # Immediately return conversion register result if in CONTINUOUS mode + # and pin has not changed if self.mode == Mode.CONTINUOUS and self._last_pin_read == pin: return self._conversion_value(self.get_last_result(True)) # Assign last pin read if in SINGLE mode or first sample in CONTINUOUS mode on this pin self._last_pin_read = pin - # Configure ADC every time before a conversion in SINGLE mode or changing channels in CONTINUOUS mode + # Configure ADC every time before a conversion in SINGLE mode + # or changing channels in CONTINUOUS mode if self.mode == Mode.SINGLE: config = _ADS1X15_CONFIG_OS_SINGLE else: From 44e135db6c041019c3495a4f970134fcc4d42bb8 Mon Sep 17 00:00:00 2001 From: WizardTim <43670403+WizardTim@users.noreply.github.com> Date: Thu, 3 Dec 2020 10:11:39 +1000 Subject: [PATCH 10/14] to --- adafruit_ads1x15/ads1x15.py | 1 + 1 file changed, 1 insertion(+) diff --git a/adafruit_ads1x15/ads1x15.py b/adafruit_ads1x15/ads1x15.py index 0ff0e8b..b7ceafe 100644 --- a/adafruit_ads1x15/ads1x15.py +++ b/adafruit_ads1x15/ads1x15.py @@ -224,3 +224,4 @@ def _read_register(self, reg, fast=False): else: i2c.write_then_readinto(bytearray([reg]), self.buf, in_end=2) return self.buf[0] << 8 | self.buf[1] + From e514aecfb8969c9a0f6608503927be07521c3424 Mon Sep 17 00:00:00 2001 From: WizardTim <43670403+WizardTim@users.noreply.github.com> Date: Thu, 3 Dec 2020 10:25:27 +1000 Subject: [PATCH 11/14] Remove trailing whitespace for psf/black --- adafruit_ads1x15/ads1x15.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/adafruit_ads1x15/ads1x15.py b/adafruit_ads1x15/ads1x15.py index b7ceafe..b406bfe 100644 --- a/adafruit_ads1x15/ads1x15.py +++ b/adafruit_ads1x15/ads1x15.py @@ -165,7 +165,7 @@ def _read(self, pin): # Assign last pin read if in SINGLE mode or first sample in CONTINUOUS mode on this pin self._last_pin_read = pin - # Configure ADC every time before a conversion in SINGLE mode + # Configure ADC every time before a conversion in SINGLE mode # or changing channels in CONTINUOUS mode if self.mode == Mode.SINGLE: config = _ADS1X15_CONFIG_OS_SINGLE @@ -224,4 +224,3 @@ def _read_register(self, reg, fast=False): else: i2c.write_then_readinto(bytearray([reg]), self.buf, in_end=2) return self.buf[0] << 8 | self.buf[1] - From 7a57f04e02fe16ef9b03e924de1f9335df6c8fdc Mon Sep 17 00:00:00 2001 From: WizardTim <43670403+WizardTim@users.noreply.github.com> Date: Tue, 15 Dec 2020 11:31:06 +1000 Subject: [PATCH 12/14] Improve acquisition timing - Remove accumulating error introduced by basing next sample time on time.monotonic call - Skip samples if scheduled time has passed to prevent over polling one conversion result in order to catch up - Pre-calculate time_next_sample in while loop comparison and sample_interval --- examples/ads1x15_fast_read.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/examples/ads1x15_fast_read.py b/examples/ads1x15_fast_read.py index 0441620..7af949c 100644 --- a/examples/ads1x15_fast_read.py +++ b/examples/ads1x15_fast_read.py @@ -25,23 +25,32 @@ ads.mode = Mode.CONTINUOUS ads.data_rate = RATE +sample_interval = 1.0 / ads.data_rate + repeats = 0 +skips = 0 data = [None] * SAMPLES -time_last_sample = time.monotonic() +time_next_sample = time.monotonic() start = time.monotonic() # Read the same channel over and over for i in range(SAMPLES): # Wait for expected conversion finish time - while time.monotonic() < (time_last_sample + (1.0 / ads.data_rate)): + while time.monotonic() < (time_next_sample): pass # Read conversion value for ADC channel - time_last_sample = time.monotonic() data[i] = chan0.value + # Loop timing + time_last_sample = time.monotonic() + time_next_sample = time_next_sample + sample_interval + if time_last_sample > (time_next_sample + sample_interval): + skips += 1 + time_next_sample = time.monotonic() + sample_interval + # Detect repeated values due to over polling if data[i] == data[i - 1]: repeats += 1 @@ -63,5 +72,6 @@ print("Actual:") print(" Polling Rate = {:8.2f} sps".format(rate_reported)) print(" {:9.2%}".format(rate_reported / RATE)) +print(" Skipped = {:5d}".format(skips)) print(" Repeats = {:5d}".format(repeats)) print(" Conversion Rate = {:8.2f} sps (estimated)".format(rate_actual)) From b1c61e21475e44caa77a6e04df258841546e001c Mon Sep 17 00:00:00 2001 From: WizardTim <43670403+WizardTim@users.noreply.github.com> Date: Thu, 17 Dec 2020 10:05:57 +1000 Subject: [PATCH 13/14] Bug fix: discard first slow channel read - Do first slow channel read(&configure/wait) and discard result before entering timed loop - Align start time with acquisition timing - Bug not entirely fixed, will still sometimes skip 1 reading when taking >1,000 samples, probably won't fix this --- examples/ads1x15_fast_read.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/ads1x15_fast_read.py b/examples/ads1x15_fast_read.py index 7af949c..07092d1 100644 --- a/examples/ads1x15_fast_read.py +++ b/examples/ads1x15_fast_read.py @@ -25,6 +25,10 @@ ads.mode = Mode.CONTINUOUS ads.data_rate = RATE +# First ADC channel read in continuous mode configures device +# and waits 2 conversion cycles +chan0.value + sample_interval = 1.0 / ads.data_rate repeats = 0 @@ -32,8 +36,8 @@ data = [None] * SAMPLES -time_next_sample = time.monotonic() start = time.monotonic() +time_next_sample = start + sample_interval # Read the same channel over and over for i in range(SAMPLES): From f6a035c089e5c303b0ac72c8284fea647cc967ac Mon Sep 17 00:00:00 2001 From: WizardTim <43670403+WizardTim@users.noreply.github.com> Date: Thu, 17 Dec 2020 10:12:18 +1000 Subject: [PATCH 14/14] Appease Pylint --- examples/ads1x15_fast_read.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ads1x15_fast_read.py b/examples/ads1x15_fast_read.py index 07092d1..e53b4c1 100644 --- a/examples/ads1x15_fast_read.py +++ b/examples/ads1x15_fast_read.py @@ -27,7 +27,7 @@ # First ADC channel read in continuous mode configures device # and waits 2 conversion cycles -chan0.value +_ = chan0.value sample_interval = 1.0 / ads.data_rate