Skip to content

Commit ba27e50

Browse files
authored
feat: implement single-shot measurements (#6)
1 parent f9613f2 commit ba27e50

17 files changed

+1129
-113
lines changed

examples/demonstrator.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,30 @@
99

1010
import argparse
1111
import logging
12-
import sys
12+
13+
# modules board and busio provide no type hints
14+
import board # type: ignore
15+
import busio # type: ignore
1316

1417
import feeph.ads1xxx
1518

16-
LH = logging.getLogger("app")
19+
LH = logging.getLogger("main")
1720

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

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

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

29-
LH.debug("start")
31+
i2c_bus = busio.I2C(scl=board.SCL, sda=board.SDA)
32+
ads1115 = feeph.ads1xxx.Ads1115(i2c_bus=i2c_bus)
3033

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

34-
LH.debug("exit")
35-
sys.exit(0)
37+
# take a single-shot measurement
38+
LH.info("measurement: %0.6fV", ads1115.get_singleshot_measurement() / (1000 * 1000))

feeph/ads1xxx/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@
55

66
# the following imports are provided for user convenience
77
# flake8: noqa: F401
8+
from feeph.ads1xxx.ads1113 import Ads1113, Ads1113Config
9+
from feeph.ads1xxx.ads1114 import Ads1114, Ads1114Config
810
from feeph.ads1xxx.ads1115 import Ads1115, Ads1115Config
11+
from feeph.ads1xxx.settings import *

feeph/ads1xxx/ads1113.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env python3
2+
"""
3+
ADS111x - Ultra-Small, Low-Power, I2C-Compatible, 860-SPS, 16-Bit ADCs
4+
With Internal Reference, Oscillator, and Programmable Comparator
5+
6+
datasheet: https://www.ti.com/lit/ds/symlink/ads1115.pdf
7+
"""
8+
9+
import logging
10+
11+
from attrs import define
12+
13+
from feeph.ads1xxx.ads111x import Ads111x, Ads111xConfig
14+
from feeph.ads1xxx.settings import CLAT, CMOD, CPOL, CQUE, DOM, DRS, MUX, PGA, SSC
15+
16+
LH = logging.getLogger('feeph.ads1xxx')
17+
18+
19+
@define
20+
class Ads1113Config(Ads111xConfig):
21+
"""
22+
The 16-bit Config register is used to control the operating mode, input
23+
selection, data rate, full-scale range, and comparator modes.
24+
"""
25+
# fmt: off
26+
ssc: SSC = SSC.NO_OP # single-shot conversion trigger
27+
dom: DOM = DOM.SSM # device operation mode
28+
drs: DRS = DRS.MODE4 # data rate setting
29+
# fmt: on
30+
31+
def as_uint16(self):
32+
# non-configurable values are set to their default
33+
value = 0b0000_0000_0000_0000
34+
value |= self.ssc.value
35+
value |= MUX.MODE0.value # no input multiplexer
36+
value |= PGA.MODE2.value # no programmable gain amplifier
37+
value |= self.dom.value
38+
value |= self.drs.value
39+
value |= CMOD.TRD.value # no comparator mode
40+
value |= CPOL.ALO.value # no comparator polarity
41+
value |= CLAT.NLC.value # no comparator latch
42+
value |= CQUE.DIS.value # no comparator queue
43+
return value
44+
45+
46+
class Ads1113(Ads111x):
47+
"""
48+
ADS1113 - Ultra-Small, Low-Power, I2C-Compatible, 860-SPS, 16-Bit ADCs
49+
With Internal Reference, Oscillator, and Programmable Comparator
50+
51+
datasheet: https://www.ti.com/lit/ds/symlink/ads1113.pdf
52+
"""
53+
_has_pga = False

feeph/ads1xxx/ads1114.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env python3
2+
"""
3+
ADS111x - Ultra-Small, Low-Power, I2C-Compatible, 860-SPS, 16-Bit ADCs
4+
With Internal Reference, Oscillator, and Programmable Comparator
5+
6+
datasheet: https://www.ti.com/lit/ds/symlink/ads1115.pdf
7+
"""
8+
9+
import logging
10+
11+
from attrs import define
12+
13+
from feeph.ads1xxx.ads111x import Ads111x, Ads111xConfig
14+
from feeph.ads1xxx.settings import CLAT, CMOD, CPOL, CQUE, DOM, DRS, MUX, PGA, SSC
15+
16+
LH = logging.getLogger('feeph.ads1xxx')
17+
18+
19+
@define
20+
class Ads1114Config(Ads111xConfig):
21+
"""
22+
The 16-bit Config register is used to control the operating mode, input
23+
selection, data rate, full-scale range, and comparator modes.
24+
"""
25+
# fmt: off
26+
ssc: SSC = SSC.NO_OP # single-shot conversion trigger
27+
pga: PGA = PGA.MODE2 # programmable gain amplifier
28+
dom: DOM = DOM.SSM # device operation mode
29+
drs: DRS = DRS.MODE4 # data rate setting
30+
cmod: CMOD = CMOD.TRD # comparator mode
31+
cpol: CPOL = CPOL.ALO # comparator polarity
32+
clat: CLAT = CLAT.NLC # comparator latch
33+
cque: CQUE = CQUE.DIS # comparator queue
34+
# fmt: on
35+
36+
def as_uint16(self):
37+
value = 0b0000_0000_0000_0000
38+
value |= self.ssc.value
39+
value |= MUX.MODE0.value # no input multiplexer
40+
value |= self.pga.value
41+
value |= self.dom.value
42+
value |= self.drs.value
43+
value |= self.cmod.value
44+
value |= self.cpol.value
45+
value |= self.clat.value
46+
value |= self.cque.value
47+
return value
48+
49+
50+
class Ads1114(Ads111x):
51+
"""
52+
ADS1113 - Ultra-Small, Low-Power, I2C-Compatible, 860-SPS, 16-Bit ADCs
53+
With Internal Reference, Oscillator, and Programmable Comparator
54+
55+
datasheet: https://www.ti.com/lit/ds/symlink/ads1114.pdf
56+
"""
57+
_has_pga = True

feeph/ads1xxx/ads1115.py

Lines changed: 37 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -8,90 +8,60 @@
88

99
import logging
1010

11-
# module busio provides no type hints
12-
import busio # type: ignore
1311
from attrs import define
14-
from feeph.i2c import BurstHandler
1512

16-
from feeph.ads1xxx.ads111x import Ads111x
13+
from feeph.ads1xxx.ads111x import Ads111xConfig
14+
from feeph.ads1xxx.ads1114 import Ads1114
15+
from feeph.ads1xxx.settings import CLAT, CMOD, CPOL, CQUE, DOM, DRS, MUX, PGA, SSC
1716

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

2019

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

38+
def as_uint16(self):
39+
value = 0b0000_0000_0000_0000
40+
value |= self.ssc.value
41+
value |= self.mux.value
42+
value |= self.pga.value
43+
value |= self.dom.value
44+
value |= self.drs.value
45+
value |= self.cmod.value
46+
value |= self.cpol.value
47+
value |= self.clat.value
48+
value |= self.cque.value
49+
return value
50+
3951

4052
DEFAULTS = {
41-
0x01: 0x8583,
42-
0x02: 0x8000,
43-
0x03: 0x7FFF,
53+
0x00: None, # conversion register (2 bytes, ro)
54+
0x01: 0x8583, # config register (2 bytes, rw)
55+
0x02: 0x8000, # lo_thresh register (2 bytes, rw)
56+
0x03: 0x7FFF, # hi_thresh register (2 bytes, rw)
4457
}
4558

4659

47-
class Ads1115(Ads111x):
48-
# 0x00 - conversion register (2 bytes, ro, default: 0x0000)
49-
# 0x01 - config register (2 bytes, rw, default: 0x8583)
50-
# 0x10 - lo_thresh register (2 bytes, rw, default: 0x0080)
51-
# 0x11 - hi_thresh register (2 bytes, rw, default: 0xFF7F)
52-
53-
def __init__(self, i2c_bus: busio.I2C):
54-
self._i2c_bus = i2c_bus
55-
self._i2c_adr = 0x48 # the I²C bus address is hardcoded
56-
57-
def get_config(self) -> Ads1115Config:
58-
with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh:
59-
value = bh.read_register(0x01, byte_count=2)
60-
params = {
61-
"OSSA": value & 0b1000_0000_0000_0000,
62-
"IMUX": value & 0b0111_0000_0000_0000,
63-
"PGA": value & 0b0000_1110_0000_0000,
64-
"MODE": value & 0b0000_0001_0000_0000,
65-
"DR": value & 0b0000_0000_1110_0000,
66-
"COMP_MOD": value & 0b0000_0000_0001_0000,
67-
"COMP_POL": value & 0b0000_0000_0000_1000,
68-
"COMP_LAT": value & 0b0000_0000_0000_0100,
69-
"COMP_QUE": value & 0b0000_0000_0000_0011,
70-
}
71-
return Ads1115Config(**params)
72-
73-
def set_config(self, config: Ads1115Config):
74-
value = 0b0000_0000_0000_0000
75-
value &= config.OSSA
76-
value &= config.IMUX
77-
value &= config.PGA
78-
value &= config.MODE
79-
value &= config.DR
80-
value &= config.COMP_MOD
81-
value &= config.COMP_POL
82-
value &= config.COMP_LAT
83-
value &= config.COMP_QUE
84-
with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh:
85-
bh.write_register(0x01, value, byte_count=2)
86-
87-
def reset_device_registers(self):
88-
with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh:
89-
for register, value in DEFAULTS.items():
90-
bh.write_register(register, value, byte_count=2)
91-
92-
# ---------------------------------------------------------------------
93-
94-
def get_measurement(self) -> int:
95-
return 0
60+
class Ads1115(Ads1114):
61+
"""
62+
ADS111x - Ultra-Small, Low-Power, I2C-Compatible, 860-SPS, 16-Bit ADCs
63+
With Internal Reference, Oscillator, and Programmable Comparator
9664
97-
# ---------------------------------------------------------------------
65+
datasheet: https://www.ti.com/lit/ds/symlink/ads1115.pdf
66+
"""
67+
_has_pga = True

feeph/ads1xxx/ads111x.py

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,70 @@
66
import logging
77
from abc import ABC, abstractmethod
88

9+
# module busio provides no type hints
10+
import busio # type: ignore
11+
from feeph.i2c import BurstHandler
12+
13+
from feeph.ads1xxx.conversions import UNIT, convert_step_to_microvolts
14+
from feeph.ads1xxx.settings import DOM, PGA, SSC
15+
916
LH = logging.getLogger('feeph.ads1xxx')
1017

1118

12-
class Ads111x(ABC):
13-
"""
19+
DEFAULTS = {
20+
0x00: None, # conversion register (2 bytes, ro)
21+
0x01: 0x8583, # config register (2 bytes, rw)
22+
0x02: 0x8000, # lo_thresh register (2 bytes, rw)
23+
0x03: 0x7FFF, # hi_thresh register (2 bytes, rw)
24+
}
25+
1426

15-
"""
27+
class Ads111xConfig(ABC):
1628

1729
@abstractmethod
18-
def get_measurement(self) -> int:
30+
def as_uint16(self):
1931
...
2032

21-
@abstractmethod
33+
34+
class Ads111x:
35+
_has_pga = False
36+
37+
def __init__(self, i2c_bus: busio.I2C):
38+
self._i2c_bus = i2c_bus
39+
self._i2c_adr = 0x48 # the I²C bus address is hardcoded
40+
2241
def reset_device_registers(self):
23-
...
42+
with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh:
43+
for register, value in DEFAULTS.items():
44+
if value is None:
45+
continue
46+
bh.write_register(register, value, byte_count=2)
47+
48+
def get_singleshot_measurement(self, config: Ads111xConfig | None = None, unit: UNIT = UNIT.MICRO) -> int:
49+
with BurstHandler(i2c_bus=self._i2c_bus, i2c_adr=self._i2c_adr) as bh:
50+
if config is None:
51+
config_uint = bh.read_register(0x01, byte_count=2)
52+
else:
53+
config_uint = config.as_uint16()
54+
LH.warning("1: config_uint = 0x%08x", config_uint)
55+
if config_uint & DOM.SSM.value:
56+
bh.write_register(0x01, config_uint | SSC.START.value, byte_count=2)
57+
LH.warning("2: config_uint = 0x%08x", config_uint | SSC.START.value)
58+
# TODO wait until measurement is ready
59+
# (0b0..._...._...._.... -> 0b1..._...._...._....)
60+
step = bh.read_register(0x00, byte_count=2)
61+
if unit == UNIT.MICRO:
62+
if self._has_pga:
63+
pga_setting = config_uint & 0b0000_1110_0000_0000
64+
for pga_mode in PGA:
65+
if pga_setting == pga_mode.value:
66+
return convert_step_to_microvolts(step, pga_mode)
67+
else:
68+
raise RuntimeError('unable to identify PGA mode ({config_uint:08X})')
69+
else:
70+
# ADS1113 has a fixed voltage range of ±2.048V
71+
return convert_step_to_microvolts(step, PGA.MODE2)
72+
else:
73+
return step
74+
else:
75+
raise RuntimeError("device is configured for continuous conversion")

feeph/ads1xxx/conversions.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env python3
2+
3+
from enum import Enum
4+
5+
from feeph.ads1xxx.settings import PGA
6+
7+
8+
class UNIT(Enum):
9+
STEPS = 0
10+
MICRO = 1
11+
12+
13+
def convert_step_to_microvolts(step: int, pga: PGA) -> int:
14+
"""
15+
convert the step value to microvolts
16+
```
17+
PGA.MODE2:
18+
-32768 -> -2048mV
19+
+32767 -> +2048mV
20+
```
21+
"""
22+
factor = {
23+
PGA.MODE0: 6144,
24+
PGA.MODE1: 4096,
25+
PGA.MODE2: 2048,
26+
PGA.MODE3: 1024,
27+
PGA.MODE4: 512,
28+
PGA.MODE5: 256,
29+
PGA.MODE6: 256, # same as MODE5
30+
PGA.MODE7: 256, # same as MODE5
31+
}
32+
# it doesn't make much sense to return a floating point value
33+
# 1 step at the highest precision level (PGA.MODE5) is 7.8µV
34+
return round(step * (factor[pga] * 1000 / 32767))

0 commit comments

Comments
 (0)