Skip to content

Commit

Permalink
feat: implement single-shot measurements (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
feeph authored Aug 6, 2024
1 parent f9613f2 commit ba27e50
Show file tree
Hide file tree
Showing 17 changed files with 1,129 additions and 113 deletions.
19 changes: 11 additions & 8 deletions examples/demonstrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,30 @@

import argparse
import logging
import sys

# modules board and busio provide no type hints
import board # type: ignore
import busio # type: ignore

import feeph.ads1xxx

LH = logging.getLogger("app")
LH = logging.getLogger("main")

if __name__ == '__main__':
logging.basicConfig(format='%(levelname).1s: %(message)s', level=logging.INFO)

parser = argparse.ArgumentParser(prog="demonstrator", description="demonstrate usage")
parser.add_argument("-i", "--input-value", type=int, default=1)
parser.add_argument("-v", "--verbose", action="store_true")
args = parser.parse_args()

if args.verbose:
LH.setLevel(level=logging.DEBUG)

LH.debug("start")
i2c_bus = busio.I2C(scl=board.SCL, sda=board.SDA)
ads1115 = feeph.ads1xxx.Ads1115(i2c_bus=i2c_bus)

value = feeph.ads1xxx.function1(args.input_value)
LH.info("Provided value: %d", value)
# we don't know what was previously configured, let's reset
ads1115.reset_device_registers()

LH.debug("exit")
sys.exit(0)
# take a single-shot measurement
LH.info("measurement: %0.6fV", ads1115.get_singleshot_measurement() / (1000 * 1000))
3 changes: 3 additions & 0 deletions feeph/ads1xxx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@

# the following imports are provided for user convenience
# flake8: noqa: F401
from feeph.ads1xxx.ads1113 import Ads1113, Ads1113Config
from feeph.ads1xxx.ads1114 import Ads1114, Ads1114Config
from feeph.ads1xxx.ads1115 import Ads1115, Ads1115Config
from feeph.ads1xxx.settings import *
53 changes: 53 additions & 0 deletions feeph/ads1xxx/ads1113.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""
ADS111x - Ultra-Small, Low-Power, I2C-Compatible, 860-SPS, 16-Bit ADCs
With Internal Reference, Oscillator, and Programmable Comparator
datasheet: https://www.ti.com/lit/ds/symlink/ads1115.pdf
"""

import logging

from attrs import define

from feeph.ads1xxx.ads111x import Ads111x, Ads111xConfig
from feeph.ads1xxx.settings import CLAT, CMOD, CPOL, CQUE, DOM, DRS, MUX, PGA, SSC

LH = logging.getLogger('feeph.ads1xxx')


@define
class Ads1113Config(Ads111xConfig):
"""
The 16-bit Config register is used to control the operating mode, input
selection, data rate, full-scale range, and comparator modes.
"""
# fmt: off
ssc: SSC = SSC.NO_OP # single-shot conversion trigger
dom: DOM = DOM.SSM # device operation mode
drs: DRS = DRS.MODE4 # data rate setting
# fmt: on

def as_uint16(self):
# non-configurable values are set to their default
value = 0b0000_0000_0000_0000
value |= self.ssc.value
value |= MUX.MODE0.value # no input multiplexer
value |= PGA.MODE2.value # no programmable gain amplifier
value |= self.dom.value
value |= self.drs.value
value |= CMOD.TRD.value # no comparator mode
value |= CPOL.ALO.value # no comparator polarity
value |= CLAT.NLC.value # no comparator latch
value |= CQUE.DIS.value # no comparator queue
return value


class Ads1113(Ads111x):
"""
ADS1113 - Ultra-Small, Low-Power, I2C-Compatible, 860-SPS, 16-Bit ADCs
With Internal Reference, Oscillator, and Programmable Comparator
datasheet: https://www.ti.com/lit/ds/symlink/ads1113.pdf
"""
_has_pga = False
57 changes: 57 additions & 0 deletions feeph/ads1xxx/ads1114.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""
ADS111x - Ultra-Small, Low-Power, I2C-Compatible, 860-SPS, 16-Bit ADCs
With Internal Reference, Oscillator, and Programmable Comparator
datasheet: https://www.ti.com/lit/ds/symlink/ads1115.pdf
"""

import logging

from attrs import define

from feeph.ads1xxx.ads111x import Ads111x, Ads111xConfig
from feeph.ads1xxx.settings import CLAT, CMOD, CPOL, CQUE, DOM, DRS, MUX, PGA, SSC

LH = logging.getLogger('feeph.ads1xxx')


@define
class Ads1114Config(Ads111xConfig):
"""
The 16-bit Config register is used to control the operating mode, input
selection, data rate, full-scale range, and comparator modes.
"""
# fmt: off
ssc: SSC = SSC.NO_OP # single-shot conversion trigger
pga: PGA = PGA.MODE2 # programmable gain amplifier
dom: DOM = DOM.SSM # device operation mode
drs: DRS = DRS.MODE4 # data rate setting
cmod: CMOD = CMOD.TRD # comparator mode
cpol: CPOL = CPOL.ALO # comparator polarity
clat: CLAT = CLAT.NLC # comparator latch
cque: CQUE = CQUE.DIS # comparator queue
# fmt: on

def as_uint16(self):
value = 0b0000_0000_0000_0000
value |= self.ssc.value
value |= MUX.MODE0.value # no input multiplexer
value |= self.pga.value
value |= self.dom.value
value |= self.drs.value
value |= self.cmod.value
value |= self.cpol.value
value |= self.clat.value
value |= self.cque.value
return value


class Ads1114(Ads111x):
"""
ADS1113 - Ultra-Small, Low-Power, I2C-Compatible, 860-SPS, 16-Bit ADCs
With Internal Reference, Oscillator, and Programmable Comparator
datasheet: https://www.ti.com/lit/ds/symlink/ads1114.pdf
"""
_has_pga = True
104 changes: 37 additions & 67 deletions feeph/ads1xxx/ads1115.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,90 +8,60 @@

import logging

# module busio provides no type hints
import busio # type: ignore
from attrs import define
from feeph.i2c import BurstHandler

from feeph.ads1xxx.ads111x import Ads111x
from feeph.ads1xxx.ads111x import Ads111xConfig
from feeph.ads1xxx.ads1114 import Ads1114
from feeph.ads1xxx.settings import CLAT, CMOD, CPOL, CQUE, DOM, DRS, MUX, PGA, SSC

LH = logging.getLogger('feeph.ads1xxx')


@define
class Ads1115Config:
class Ads1115Config(Ads111xConfig):
"""
The 16-bit Config register is used to control the operating mode, input
selection, data rate, full-scale range, and comparator modes.
"""
# fmt: off
OSSA: int # 0b#..._...._...._.... status or single shot start
IMUX: int # 0b.###_...._...._.... input multiplexer configuration
PGA: int # 0b...._###._...._.... programmable gain amplifier
MODE: int # 0b...._...#_...._.... operating mode
DR: int # 0b...._...._###._.... data rate
COMP_MOD: int # 0b...._...._...#_.... comparator mode
COMP_POL: int # 0b...._...._...._#... comparator polarity
COMP_LAT: int # 0b...._...._...._.#.. latching comparator
COMP_QUE: int # 0b...._...._...._..## comparator queue & disable
ssc: SSC = SSC.NO_OP # single-shot conversion trigger
mux: MUX = MUX.MODE0 # input multiplexer
pga: PGA = PGA.MODE2 # programmable gain amplifier
dom: DOM = DOM.SSM # device operation mode
drs: DRS = DRS.MODE4 # data rate setting
cmod: CMOD = CMOD.TRD # comparator mode
cpol: CPOL = CPOL.ALO # comparator polarity
clat: CLAT = CLAT.NLC # comparator latch
cque: CQUE = CQUE.DIS # comparator queue
# fmt: on

def as_uint16(self):
value = 0b0000_0000_0000_0000
value |= self.ssc.value
value |= self.mux.value
value |= self.pga.value
value |= self.dom.value
value |= self.drs.value
value |= self.cmod.value
value |= self.cpol.value
value |= self.clat.value
value |= self.cque.value
return value


DEFAULTS = {
0x01: 0x8583,
0x02: 0x8000,
0x03: 0x7FFF,
0x00: None, # conversion register (2 bytes, ro)
0x01: 0x8583, # config register (2 bytes, rw)
0x02: 0x8000, # lo_thresh register (2 bytes, rw)
0x03: 0x7FFF, # hi_thresh register (2 bytes, rw)
}


class Ads1115(Ads111x):
# 0x00 - conversion register (2 bytes, ro, default: 0x0000)
# 0x01 - config register (2 bytes, rw, default: 0x8583)
# 0x10 - lo_thresh register (2 bytes, rw, default: 0x0080)
# 0x11 - hi_thresh register (2 bytes, rw, default: 0xFF7F)

def __init__(self, i2c_bus: busio.I2C):
self._i2c_bus = i2c_bus
self._i2c_adr = 0x48 # the I²C bus address is hardcoded

def get_config(self) -> Ads1115Config:
with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh:
value = bh.read_register(0x01, byte_count=2)
params = {
"OSSA": value & 0b1000_0000_0000_0000,
"IMUX": value & 0b0111_0000_0000_0000,
"PGA": value & 0b0000_1110_0000_0000,
"MODE": value & 0b0000_0001_0000_0000,
"DR": value & 0b0000_0000_1110_0000,
"COMP_MOD": value & 0b0000_0000_0001_0000,
"COMP_POL": value & 0b0000_0000_0000_1000,
"COMP_LAT": value & 0b0000_0000_0000_0100,
"COMP_QUE": value & 0b0000_0000_0000_0011,
}
return Ads1115Config(**params)

def set_config(self, config: Ads1115Config):
value = 0b0000_0000_0000_0000
value &= config.OSSA
value &= config.IMUX
value &= config.PGA
value &= config.MODE
value &= config.DR
value &= config.COMP_MOD
value &= config.COMP_POL
value &= config.COMP_LAT
value &= config.COMP_QUE
with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh:
bh.write_register(0x01, value, byte_count=2)

def reset_device_registers(self):
with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh:
for register, value in DEFAULTS.items():
bh.write_register(register, value, byte_count=2)

# ---------------------------------------------------------------------

def get_measurement(self) -> int:
return 0
class Ads1115(Ads1114):
"""
ADS111x - Ultra-Small, Low-Power, I2C-Compatible, 860-SPS, 16-Bit ADCs
With Internal Reference, Oscillator, and Programmable Comparator
# ---------------------------------------------------------------------
datasheet: https://www.ti.com/lit/ds/symlink/ads1115.pdf
"""
_has_pga = True
64 changes: 58 additions & 6 deletions feeph/ads1xxx/ads111x.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,70 @@
import logging
from abc import ABC, abstractmethod

# module busio provides no type hints
import busio # type: ignore
from feeph.i2c import BurstHandler

from feeph.ads1xxx.conversions import UNIT, convert_step_to_microvolts
from feeph.ads1xxx.settings import DOM, PGA, SSC

LH = logging.getLogger('feeph.ads1xxx')


class Ads111x(ABC):
"""
DEFAULTS = {
0x00: None, # conversion register (2 bytes, ro)
0x01: 0x8583, # config register (2 bytes, rw)
0x02: 0x8000, # lo_thresh register (2 bytes, rw)
0x03: 0x7FFF, # hi_thresh register (2 bytes, rw)
}


"""
class Ads111xConfig(ABC):

@abstractmethod
def get_measurement(self) -> int:
def as_uint16(self):
...

@abstractmethod

class Ads111x:
_has_pga = False

def __init__(self, i2c_bus: busio.I2C):
self._i2c_bus = i2c_bus
self._i2c_adr = 0x48 # the I²C bus address is hardcoded

def reset_device_registers(self):
...
with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh:
for register, value in DEFAULTS.items():
if value is None:
continue
bh.write_register(register, value, byte_count=2)

def get_singleshot_measurement(self, config: Ads111xConfig | None = None, unit: UNIT = UNIT.MICRO) -> int:
with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh:
if config is None:
config_uint = bh.read_register(0x01, byte_count=2)
else:
config_uint = config.as_uint16()
LH.warning("1: config_uint = 0x%08x", config_uint)
if config_uint & DOM.SSM.value:
bh.write_register(0x01, config_uint | SSC.START.value, byte_count=2)
LH.warning("2: config_uint = 0x%08x", config_uint | SSC.START.value)
# TODO wait until measurement is ready
# (0b0..._...._...._.... -> 0b1..._...._...._....)
step = bh.read_register(0x00, byte_count=2)
if unit == UNIT.MICRO:
if self._has_pga:
pga_setting = config_uint & 0b0000_1110_0000_0000
for pga_mode in PGA:
if pga_setting == pga_mode.value:
return convert_step_to_microvolts(step, pga_mode)
else:
raise RuntimeError('unable to identify PGA mode ({config_uint:08X})')
else:
# ADS1113 has a fixed voltage range of ±2.048V
return convert_step_to_microvolts(step, PGA.MODE2)
else:
return step
else:
raise RuntimeError("device is configured for continuous conversion")
34 changes: 34 additions & 0 deletions feeph/ads1xxx/conversions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env python3

from enum import Enum

from feeph.ads1xxx.settings import PGA


class UNIT(Enum):
STEPS = 0
MICRO = 1


def convert_step_to_microvolts(step: int, pga: PGA) -> int:
"""
convert the step value to microvolts
```
PGA.MODE2:
-32768 -> -2048mV
+32767 -> +2048mV
```
"""
factor = {
PGA.MODE0: 6144,
PGA.MODE1: 4096,
PGA.MODE2: 2048,
PGA.MODE3: 1024,
PGA.MODE4: 512,
PGA.MODE5: 256,
PGA.MODE6: 256, # same as MODE5
PGA.MODE7: 256, # same as MODE5
}
# it doesn't make much sense to return a floating point value
# 1 step at the highest precision level (PGA.MODE5) is 7.8µV
return round(step * (factor[pga] * 1000 / 32767))
Loading

0 comments on commit ba27e50

Please sign in to comment.