Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Comparator functionality #98

Merged
merged 12 commits into from
Aug 6, 2024
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/pycqa/pylint
rev: v2.17.4
rev: v3.2.6
hooks:
- id: pylint
name: pylint (library code)
Expand Down
8 changes: 7 additions & 1 deletion adafruit_ads1x15/ads1015.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ def rate_config(self) -> Dict[int, int]:
def _data_rate_default(self) -> Literal[1600]:
return 1600

def _comp_low_thres_default(self) -> Literal[0x8000]:
return 0x8000

def _comp_high_thres_default(self) -> Literal[0x7FF0]:
return 0x7FF0

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def _comp_low_thres_default(self) -> Literal[0x8000]:
return 0x8000
def _comp_high_thres_default(self) -> Literal[0x7FF0]:
return 0x7FF0

def _conversion_value(self, raw_adc: int) -> int:
value = struct.unpack(">h", raw_adc.to_bytes(2, "big"))[0]
return value >> 4
return value
Comment on lines -72 to +73
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't change this. You'll change how read() works otherwise. Only .value needs to be the 16bit standard.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the ads1015 flow, I believe there are 2 unnecessary bit shifts. This is the right bit shift, which is immediately cancelled out by the left bit shift in the value property of analog_in.py. I removed both of these. There is no need to take in a 16-bit number, convert to 12, convert back to 16, and then report as 16. Removing both keeps .value at 16-bits for both ads1015 and ads1115.

6 changes: 6 additions & 0 deletions adafruit_ads1x15/ads1115.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ def rate_config(self) -> Dict[int, int]:
def _data_rate_default(self) -> Literal[128]:
return 128

def _comp_low_thres_default(self) -> Literal[0x8000]:
return 0x8000

def _comp_high_thres_default(self) -> Literal[0x7FFF]:
return 0x7FFF

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def _comp_low_thres_default(self) -> Literal[0x8000]:
return 0x8000
def _comp_high_thres_default(self) -> Literal[0x7FFF]:
return 0x7FFF

def _conversion_value(self, raw_adc: int) -> int:
value = struct.unpack(">h", raw_adc.to_bytes(2, "big"))[0]
return value
105 changes: 103 additions & 2 deletions adafruit_ads1x15/ads1x15.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,17 @@
_ADS1X15_DEFAULT_ADDRESS = const(0x48)
_ADS1X15_POINTER_CONVERSION = const(0x00)
_ADS1X15_POINTER_CONFIG = const(0x01)
_ADS1X15_POINTER_LO_THRES = const(0x02)
_ADS1X15_POINTER_HI_THRES = const(0x03)

_ADS1X15_CONFIG_OS_SINGLE = const(0x8000)
_ADS1X15_CONFIG_MUX_OFFSET = const(12)
_ADS1X15_CONFIG_COMP_QUE_DISABLE = const(0x0003)
_ADS1X15_CONFIG_COMP_QUEUE = {
0: 0x0003,
1: 0x0000,
2: 0x0001,
4: 0x0002,
}
_ADS1X15_CONFIG_GAIN = {
2 / 3: 0x0000,
1: 0x0200,
Expand Down Expand Up @@ -66,15 +74,30 @@ class ADS1x15:
:param int data_rate: The data rate for ADC conversion in samples per second.
Default value depends on the device.
:param Mode mode: The conversion mode, defaults to `Mode.SINGLE`.
:param int comparator_queue_length: The number of successive conversions exceeding
the comparator threshold before asserting ALERT/RDY pin.
Defaults to 0 (comparator function disabled).
:param int comparator_low_threshold: Voltage limit under which comparator de-asserts
ALERT/RDY pin. Must be lower than high threshold to use comparator
function. Value is 12-bit, 2's complement stored in
16-bit register where 4 LSBs are 0. Defaults to 0x8000 (decimal -32768).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function. Value is 12-bit, 2's complement stored in
16-bit register where 4 LSBs are 0. Defaults to 0x8000 (decimal -32768).
function. Value is a signed 17-bit integer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized I forgot to update this comment in the higher level (ads1x15) class to be more generic, since the subclasses/chips handle this differently.
However, even for the ads1115, the conversion register (ADC data) and the threshold registers on the chip are only 16 bits total, whether or not you think of them as signed or unsigned, so there are only 2^16 (0 to 65535) possible values. So I don't understand why call it a 17-bit int? It would seem like python can handle it either way, as in the widths of ints don't have to be specified. But I agree that the comment should be correct so everyone understands.

:param int comparator_high_threshold: Voltage limit over which comparator asserts
ALERT/RDY pin. Must be higher than low threshold to use comparator
function. Value is 12-bit, 2's complement stored in
16-bit register where 4 LSBs are 0. Defaults to 0x7FF0 (decimal 32752).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function. Value is 12-bit, 2's complement stored in
16-bit register where 4 LSBs are 0. Defaults to 0x7FF0 (decimal 32752).
function. Value is a signed 17-bit integer.

:param int address: The I2C address of the device.
"""

# pylint: disable=too-many-instance-attributes
def __init__(
self,
i2c: I2C,
gain: float = 1,
data_rate: Optional[int] = None,
mode: int = Mode.SINGLE,
comparator_queue_length: int = 0,
comparator_low_threshold: Optional[int] = None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
comparator_low_threshold: Optional[int] = None,
comparator_low_threshold: int = -32768,

comparator_high_threshold: Optional[int] = None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
comparator_high_threshold: Optional[int] = None,
comparator_high_threshold: int = 32767,

address: int = _ADS1X15_DEFAULT_ADDRESS,
):
# pylint: disable=too-many-arguments
Expand All @@ -83,7 +106,18 @@ def __init__(
self.gain = gain
self.data_rate = self._data_rate_default() if data_rate is None else data_rate
self.mode = mode
self.comparator_queue_length = comparator_queue_length
self.i2c_device = I2CDevice(i2c, address)
self.comparator_low_threshold = (
self._comp_low_thres_default()
if comparator_low_threshold is None
else comparator_low_threshold
)
self.comparator_high_threshold = (
self._comp_high_thres_default()
if comparator_high_threshold is None
else comparator_high_threshold
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default is the same for both because the lower four bits are 1s in the high threshold.

Suggested change
self.comparator_low_threshold = (
self._comp_low_thres_default()
if comparator_low_threshold is None
else comparator_low_threshold
)
self.comparator_high_threshold = (
self._comp_high_thres_default()
if comparator_high_threshold is None
else comparator_high_threshold
)
self.comparator_low_threshold = comparator_low_threshold
self.comparator_high_threshold = comparator_high_threshold

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct. I mistakenly went to some effort to separate them when I didn't need to.


@property
def bits(self) -> int:
Expand Down Expand Up @@ -131,6 +165,61 @@ def gains(self) -> List[float]:
g.sort()
return g

@property
def comparator_queue_length(self) -> int:
"""The ADC comparator queue length."""
return self._comparator_queue_length

@comparator_queue_length.setter
def comparator_queue_length(self, comparator_queue_length: int) -> None:
possible_comp_queue_lengths = self.comparator_queue_lengths
if comparator_queue_length not in possible_comp_queue_lengths:
raise ValueError(
"Comparator Queue must be one of: {}".format(
possible_comp_queue_lengths
)
)
self._comparator_queue_length = comparator_queue_length

@property
def comparator_queue_lengths(self) -> List[int]:
"""Possible comparator queue length settings."""
g = list(_ADS1X15_CONFIG_COMP_QUEUE.keys())
g.sort()
return g

@property
def comparator_low_threshold(self) -> int:
"""The ADC Comparator Lower Limit Threshold."""
return self._comparator_low_threshold

@property
def comparator_high_threshold(self) -> int:
"""The ADC Comparator Higher Limit Threshold."""
return self._comparator_high_threshold

@comparator_low_threshold.setter
def comparator_low_threshold(self, value: int) -> None:
"""Set comparator low threshold value for ADS1015 ADC
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Set comparator low threshold value for ADS1015 ADC
"""Comparator low threshold value


:param int value: 16-bit signed integer to write to register
"""
if value < 0 or value > 65535:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the right range to check against since it is signed?

Suggested change
if value < 0 or value > 65535:
if value < -32768 or value > 32767:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are technically correct, but python will look at 0xFFFF as 65535 and say that it is out of range, instead of looking at it as -1 and say that it is in range. I feel like what I did was a workaround to make things work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just discovered "a_bytes_big = an_int.to_bytes(2, 'big', signed = True)" that should convert between signed integers and bytes easier. I'll try to work that in.

raise ValueError("Comparator Threshold value must be between 0 and 65535")
self._comparator_low_threshold = value
self._write_register(_ADS1X15_POINTER_LO_THRES, self.comparator_low_threshold)

@comparator_high_threshold.setter
def comparator_high_threshold(self, value: int) -> None:
"""Set comparator high threshold value for ADS1015 ADC
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Set comparator high threshold value for ADS1015 ADC
"""Comparator high threshold value


:param int value: 16-bit signed integer to write to register
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:param int value: 16-bit signed integer to write to register
:param int value: 17-bit signed integer to write to register. The lowest bit is dropped.

"""
if value < 0 or value > 65535:
raise ValueError("Comparator Threshold value must be between 0 and 65535")
self._comparator_high_threshold = value
self._write_register(_ADS1X15_POINTER_HI_THRES, self.comparator_high_threshold)

@property
def mode(self) -> int:
"""The ADC conversion mode."""
Expand All @@ -157,6 +246,18 @@ def _data_rate_default(self) -> int:
"""
raise NotImplementedError("Subclasses must implement _data_rate_default!")

def _comp_low_thres_default(self) -> int:
"""Retrieve the default comparator low threshold for this ADC (in 16-bit signed int).
Should be implemented by subclasses.
"""
raise NotImplementedError("Subclasses must implement _comp_low_thres_default!")

def _comp_high_thres_default(self) -> int:
"""Retrieve the default comparator high threshold for this ADC (in 16-bit signed int).
Should be implemented by subclasses.
"""
raise NotImplementedError("Subclasses must implement _comp_high_thres_default!")

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def _comp_low_thres_default(self) -> int:
"""Retrieve the default comparator low threshold for this ADC (in 16-bit signed int).
Should be implemented by subclasses.
"""
raise NotImplementedError("Subclasses must implement _comp_low_thres_default!")
def _comp_high_thres_default(self) -> int:
"""Retrieve the default comparator high threshold for this ADC (in 16-bit signed int).
Should be implemented by subclasses.
"""
raise NotImplementedError("Subclasses must implement _comp_high_thres_default!")

def _conversion_value(self, raw_adc: int) -> int:
"""Subclasses should override this function that takes the 16 raw ADC
values of a conversion result and returns a signed integer value.
Expand All @@ -183,7 +284,7 @@ def _read(self, pin: Pin) -> int:
config |= _ADS1X15_CONFIG_GAIN[self.gain]
config |= self.mode
config |= self.rate_config[self.data_rate]
config |= _ADS1X15_CONFIG_COMP_QUE_DISABLE
config |= _ADS1X15_CONFIG_COMP_QUEUE[self.comparator_queue_length]
self._write_register(_ADS1X15_POINTER_CONFIG, config)

# Wait for conversion to complete
Expand Down
45 changes: 41 additions & 4 deletions adafruit_ads1x15/analog_in.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,49 @@ def value(self) -> int:
Even if the underlying analog to digital converter (ADC) is
lower resolution, the value is 16-bit.
"""
return self._ads.read(
self._pin_setting, is_differential=self.is_differential
) << (16 - self._ads.bits)
return self._ads.read(self._pin_setting, is_differential=self.is_differential)

@property
def voltage(self) -> float:
"""Returns the voltage from the ADC pin as a floating point value."""
volts = self.value * _ADS1X15_PGA_RANGE[self._ads.gain] / 32767
volts = self.convert_to_voltage(self.value)
return volts

def convert_to_value(self, volts: float) -> int:
"""Calculates 12-bit integer for threshold registers from voltage level input"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Calculates 12-bit integer for threshold registers from voltage level input"""
"""Calculates a standard 16-bit integer value for a given voltage"""


# Convert 2's complement of signed int if number is negative
tannewt marked this conversation as resolved.
Show resolved Hide resolved
if volts > 0:
value = round(
(volts / _ADS1X15_PGA_RANGE[self._ads.gain])
* ((1 << (self._ads.bits - 1)) - 1)
)
else:
value = round(
(volts / _ADS1X15_PGA_RANGE[self._ads.gain])
* (1 << (self._ads.bits - 1))
)
value += 1 << self._ads.bits

# Need to bit shift if value is only 12-bits
value <<= 16 - self._ads.bits
return value

def convert_to_voltage(self, value_int: int) -> float:
"""Calculates voltage from 16-bit ADC reading"""

if value_int & 0x8000:
# Need to convert negative number through 2's complement
value_int -= 0x10000

# Need to bit shift if value is only 12-bits
value_int >>= 16 - self._ads.bits
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again. Don't worry about two's complement or the numbers of bits. This should be 16 bit unsigned or "17 bit" signed to voltage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are on different pages as far as how to represent value. From "Signed number representations" on Wikipedia, 8-bit signed goes from -127 to 127. 8-bit unsigned goes from 0 to 255. I think value should match the registers in the datasheet, which is 16 bit two's complement or signed.

Can you explain 17-bit to me?

Voltage is a float since we want it to have decimal places.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value for reading from an adc has been defined as 16-bit unsigned since we added the AnalogIn API. If you want this resolution signed, then you need to add another bit for the sign. That would make it 17 total bits. I realize that the comparator will only do 16-bit signed. This conversion from voltage to value should handle the full 16-bit unsigned range still though.


volts = float(value_int)
volts = (
volts
* _ADS1X15_PGA_RANGE[self._ads.gain]
/ (0x7FFF >> (16 - self._ads.bits))
)

return volts
43 changes: 43 additions & 0 deletions examples/ads1x15_comparator_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT

import time
import board
import busio
import countio

import adafruit_ads1x15.ads1015 as ADS

# import adafruit_ads1x15.ads1115 as ADS
from adafruit_ads1x15.analog_in import AnalogIn

# Create the I2C bus
i2c = busio.I2C(board.SCL, board.SDA)

# Create the ADS object
ads = ADS.ADS1015(i2c)
# ads = ADS.ADS1115(i2c)

# Create a single-ended channel on Pin 0
# Max counts for ADS1015 = 2047
# ADS1115 = 32767
chan = AnalogIn(ads, ADS.P0)

# Create Interrupt-driven input to track comparator changes
int_pin = countio.Counter(board.GP9, edge=countio.Edge.RISE)

# Set comparator to assert after 1 ADC conversion
ads.comparator_queue_length = 1

# Set comparator low threshold to 2V
ads.comparator_low_threshold = chan.convert_to_value(2.000)
# Set comparator high threshold to 2.002V. High threshold must be above low threshold
ads.comparator_high_threshold = chan.convert_to_value(2.002)

count = 0
while True:
print(chan.value, chan.voltage) # This initiates new ADC reading
if int_pin.count > count:
print("Comparator Triggered")
count = int_pin.count
time.sleep(2)