# SPDX-FileCopyrightText: Copyright (c) 2023 Jose D. Montoya
#
# SPDX-License-Identifier: MIT
"""
`dps310`
================================================================================
MicroPython Driver for the DP310 Barametric Sensor
* Author: Jose D. Montoya
Implementation Notes
--------------------
**Software and Dependencies:**
This library depends on Micropython
"""
# pylint: disable=line-too-long
import time
import math
import struct
from micropython import const
from micropython_dps310.i2c_helpers import CBits, RegisterStruct
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/jposada202020/MicroPython_DPS310.git"
_DEVICE_ID = const(0x0D)
_PRS_CFG = const(0x06)
_TMP_CFG = const(0x07)
_MEAS_CFG = const(0x08)
_CFGREG = const(0x09)
_RESET = const(0x0C)
_TMPCOEFSRCE = const(0x28) # Temperature calibration src
# DPS310 Pressure Oversampling Rate
SAMPLE_PER_SECOND_1 = const(0b000) # 1 time (Pressure Low Precision)
SAMPLE_PER_SECOND_2 = const(0b001) # 2 times (Pressure Low Power)
SAMPLE_PER_SECOND_4 = const(0b010) # 4 times
SAMPLE_PER_SECOND_8 = const(0b011) # 8 times
SAMPLE_PER_SECOND_16 = const(0b100) # 16 times (Pressure Standard).**
SAMPLE_PER_SECOND_32 = const(0b101) # 32 times **
SAMPLE_PER_SECOND_64 = const(0b110) # 64 times (Pressure High Precision) **
SAMPLE_PER_SECOND_128 = const(0b111) # 128 times **
oversamples_values = (
SAMPLE_PER_SECOND_1,
SAMPLE_PER_SECOND_2,
SAMPLE_PER_SECOND_4,
SAMPLE_PER_SECOND_8,
SAMPLE_PER_SECOND_16,
SAMPLE_PER_SECOND_32,
SAMPLE_PER_SECOND_64,
SAMPLE_PER_SECOND_128,
)
# DPS310 Pressure Sample Rate
RATE_1_HZ = const(0b000)
RATE_2_HZ = const(0b001)
RATE_4_HZ = const(0b010)
RATE_8_HZ = const(0b011)
RATE_16_HZ = const(0b100)
RATE_32_HZ = const(0b101)
RATE_64_HZ = const(0b110)
RATE_128_HZ = const(0b111)
rates_values = (
RATE_1_HZ,
RATE_2_HZ,
RATE_4_HZ,
RATE_8_HZ,
RATE_16_HZ,
RATE_32_HZ,
RATE_64_HZ,
RATE_128_HZ,
)
IDLE = const(0b000)
ONE_PRESSURE = const(0b001)
ONE_TEMPERATURE = const(0b010)
CONT_PRESSURE = const(0b101)
CONT_TEMP = const(0b110)
CONT_PRESTEMP = const(0b111)
mode_values = (
IDLE,
ONE_PRESSURE,
ONE_TEMPERATURE,
CONT_PRESSURE,
CONT_TEMP,
CONT_PRESTEMP,
)
[docs]class DPS310:
"""Main class for the Sensor
:param ~machine.I2C i2c: The I2C bus the DPS310 is connected to.
:param int address: The I2C device address. Defaults to :const:`0x77`
:raises RuntimeError: if the sensor is not found
**Quickstart: Importing and using the device**
Here is an example of using the :class:`micropython_dps310.DPS310` class.
First you will need to import the libraries to use the sensor
.. code-block:: python
from machine import Pin, I2C
import micropython_dps310.dps310 as dps310
Once this is done you can define your `machine.I2C` object and define your sensor object
.. code-block:: python
i2c = I2C(1, sda=Pin(2), scl=Pin(3))
dps = dps310.DPS310(i2c)
Now you have access to the :attr:`pressure` attribute
.. code-block:: python
press = dps.pressure
"""
# Register definitions
_device_id = RegisterStruct(_DEVICE_ID, ">B")
_reset_register = RegisterStruct(_RESET, ">B")
_press_conf_reg = RegisterStruct(_PRS_CFG, ">B")
_temp_conf_reg = RegisterStruct(_TMP_CFG, ">B")
_sensor_operation_mode = RegisterStruct(_MEAS_CFG, ">B")
# Register 0x06 Pressure Configuration
# | ---- | PM_RATE(2) | PM_RATE(1)| PM_RATE(0) | PM_PRC(3) | PM_PRC(2) | PM_PRC(1) | PM_PRC(0) |
_pressure_oversample = CBits(4, _PRS_CFG, 0)
_pressure_rate = CBits(3, _PRS_CFG, 4)
# Register 0x07 Temperature Configuration
# | TMP_EXT | PM_RATE(2) | PM_RATE(1)| PM_RATE(0) | PM_PRC(3) | PM_PRC(2) | PM_PRC(1) | PM_PRC(0) |
_temperature_oversample = CBits(4, _TMP_CFG, 0)
_temperature_rate = CBits(3, _TMP_CFG, 4)
_temperature_external_source = CBits(1, _TMP_CFG, 7)
# Register 0x08 Sensor Operating Mode and Status
# | COEF_RDY | SENSOR_RDY | TMP_RDY | PRS_RDY | ---- | MEAS_CTRL(2) | MEAS_CTRL(1) | MEAS_CTRL(0) |
_sensor_mode = CBits(3, _MEAS_CFG, 0)
_pressure_ready = CBits(1, _MEAS_CFG, 4)
_sensor_ready = CBits(1, _MEAS_CFG, 6)
_temp_ready = CBits(1, _MEAS_CFG, 5)
_coefficients_ready = CBits(1, _MEAS_CFG, 7)
# Register 0x09 Sensor interreupts
# | INT_HL | INT_FIFO | INT_TMP | INT_PRS | T_SHIFT | P_SHIFT | FIFO_EN | SPI_MODE |
_t_shift = CBits(1, _CFGREG, 3)
_p_shift = CBits(1, _CFGREG, 2)
_raw_pressure = CBits(24, 0x00, 0, 3, False)
_raw_temperature = CBits(24, 0x03, 0, 3, False)
_calib_coeff_temp_src_bit = CBits(1, _TMPCOEFSRCE, 7)
_reg0e = CBits(8, 0x0E, 0)
_reg0f = CBits(8, 0x0F, 0)
_reg62 = CBits(8, 0x62, 0)
_measurement_times_table = {
0: 3.6,
1: 5.2,
2: 8.4,
3: 14.8,
4: 27.6,
5: 53.2,
6: 104.4,
7: 206.8,
}
_calib_coeff_temp_src_bit = CBits(1, _TMPCOEFSRCE, 7)
_soft_reset = CBits(4, 0x0C, 0)
def __init__(self, i2c, address=0x77) -> None:
self._i2c = i2c
self._address = address
if self._device_id != 0x10:
raise RuntimeError("Failed to find the DPS310 sensor!")
self._pressure_scale = None
self._temp_scale = None
self._oversample_scalefactor = (
524288.0,
1572864.0,
3670016.0,
7864320.0,
253952.0,
516096.0,
1040384.0,
2088960.0,
)
self._sea_level_pressure = 1013.25
self._correct_temp()
self._read_calibration()
self._temp_measurement_src_bit = self._calib_coeff_temp_src_bit
self.pressure_oversample = RATE_64_HZ
self.temperature_oversample = RATE_64_HZ
self._sensor_mode = CONT_PRESTEMP
self._wait_temperature_ready()
self._wait_pressure_ready()
@property
def pressure_oversample(self) -> str:
"""
Pressure Oversample. In order to achieve a higher precision, the sensor DPS310
will read multiple times ( oversampling ), and combine the readings into one result.
This increases the current consumption and also the measurement time, reducing the
maximum possible measurement rate. It is necessary to balance the accuracy and data rate
required for each application with the allowable current consumption.
+------------------------------------------+-------------------------------------------------------------------+
| Mode | Value |
+==========================================+===================================================================+
| :py:const:`dps310.SAMPLE_PER_SECOND_1` | :py:const:`0b000` # 1 time (Pressure Low Precision) |
+------------------------------------------+-------------------------------------------------------------------+
| :py:const:`dps310.SAMPLE_PER_SECOND_2` | :py:const:`0b001` # 2 times (Pressure Low Power) |
+------------------------------------------+-------------------------------------------------------------------+
| :py:const:`dps310.SAMPLE_PER_SECOND_4` | :py:const:`0b010` # 4 times |
+------------------------------------------+-------------------------------------------------------------------+
| :py:const:`dps310.SAMPLE_PER_SECOND_8` | :py:const:`0b011` # 8 times |
+------------------------------------------+-------------------------------------------------------------------+
| :py:const:`dps310.SAMPLE_PER_SECOND_16` | :py:const:`0b100` # 16 times (Pressure Standard).** |
+------------------------------------------+-------------------------------------------------------------------+
| :py:const:`dps310.SAMPLE_PER_SECOND_32` | :py:const:`0b101` # 32 times ** |
+------------------------------------------+-------------------------------------------------------------------+
| :py:const:`dps310.SAMPLE_PER_SECOND_64` | :py:const:`0b110` # 64 times (Pressure High Precision) ** |
+------------------------------------------+-------------------------------------------------------------------+
| :py:const:`dps310.SAMPLE_PER_SECOND_128` | :py:const:`0b111` # 128 times ** |
+------------------------------------------+-------------------------------------------------------------------+
"""
values = (
"SAMPLE_PER_SECOND_1",
"SAMPLE_PER_SECOND_2",
"SAMPLE_PER_SECOND_4",
"SAMPLE_PER_SECOND_8",
"SAMPLE_PER_SECOND_16",
"SAMPLE_PER_SECOND_32",
"SAMPLE_PER_SECOND_64",
"SAMPLE_PER_SECOND_128",
)
return values[self._pressure_oversample]
@pressure_oversample.setter
def pressure_oversample(self, value: int) -> None:
if value not in oversamples_values:
raise ValueError("Value must be a valid oversample setting")
self._pressure_oversample = value
self._p_shift = value > SAMPLE_PER_SECOND_8
self._pressure_scale = self._oversample_scalefactor[value]
@property
def pressure_rate(self) -> str:
"""
+--------------------------------+--------------------------+
| Mode | Value |
+================================+==========================+
| :py:const:`dps310.RATE_1_HZ` | :py:const:`0b000` |
+--------------------------------+--------------------------+
| :py:const:`dps310.RATE_2_HZ` | :py:const:`0b001` |
+--------------------------------+--------------------------+
| :py:const:`dps310.RATE_4_HZ` | :py:const:`0b010` |
+--------------------------------+--------------------------+
| :py:const:`dps310.RATE_8_HZ` | :py:const:`0b011` |
+--------------------------------+--------------------------+
| :py:const:`dps310.RATE_16_HZ` | :py:const:`0b100` |
+--------------------------------+--------------------------+
| :py:const:`dps310.RATE_32_HZ` | :py:const:`0b101` |
+--------------------------------+--------------------------+
| :py:const:`dps310.RATE_64_HZ` | :py:const:`0b110` |
+--------------------------------+--------------------------+
| :py:const:`dps310.RATE_128_HZ` | :py:const:`0b111` |
+--------------------------------+--------------------------+
"""
values = (
"RATE_1_HZ",
"RATE_2_HZ",
"RATE_4_HZ",
"RATE_8_HZ",
"RATE_16_HZ",
"RATE_32_HZ",
"RATE_64_HZ",
"RATE_128_HZ",
)
return values[self._pressure_rate]
@pressure_rate.setter
def pressure_rate(self, value: int) -> None:
if value not in rates_values:
raise ValueError("Value must be a valid rate setting")
self._pressure_rate = value
@property
def temperature_oversample(self) -> str:
"""
Temperature Oversample. In order to achieve a higher precision, the sensor DPS310
will read multiple times ( oversampling ), and combine the readings into one result.
This increases the current consumption and also the measurement time, reducing the
maximum possible measurement rate. It is necessary to balance the accuracy and data rate
required for each application with the allowable current consumption.
+------------------------------------------+---------------------------------------+
| Mode | Value |
+==========================================+=======================================+
| :py:const:`dps310.SAMPLE_PER_SECOND_1` | :py:const:`0b000` # 1 time |
+------------------------------------------+---------------------------------------+
| :py:const:`dps310.SAMPLE_PER_SECOND_2` | :py:const:`0b001` # 2 times |
+------------------------------------------+---------------------------------------+
| :py:const:`dps310.SAMPLE_PER_SECOND_4` | :py:const:`0b010` # 4 times |
+------------------------------------------+---------------------------------------+
| :py:const:`dps310.SAMPLE_PER_SECOND_8` | :py:const:`0b011` # 8 times |
+------------------------------------------+---------------------------------------+
| :py:const:`dps310.SAMPLE_PER_SECOND_16` | :py:const:`0b100` # 16 times |
+------------------------------------------+---------------------------------------+
| :py:const:`dps310.SAMPLE_PER_SECOND_32` | :py:const:`0b101` # 32 times |
+------------------------------------------+---------------------------------------+
| :py:const:`dps310.SAMPLE_PER_SECOND_64` | :py:const:`0b110` # 64 times |
+------------------------------------------+---------------------------------------+
| :py:const:`dps310.SAMPLE_PER_SECOND_128` | :py:const:`0b111` # 128 times |
+------------------------------------------+---------------------------------------+
"""
values = (
"SAMPLE_PER_SECOND_1",
"SAMPLE_PER_SECOND_2",
"SAMPLE_PER_SECOND_4",
"SAMPLE_PER_SECOND_8",
"SAMPLE_PER_SECOND_16",
"SAMPLE_PER_SECOND_32",
"SAMPLE_PER_SECOND_64",
"SAMPLE_PER_SECOND_128",
)
return values[self._temperature_oversample]
@temperature_oversample.setter
def temperature_oversample(self, value: int) -> None:
if value not in oversamples_values:
raise ValueError("Value must be a valid oversample setting")
self._temperature_oversample = value
self._temp_scale = self._oversample_scalefactor[value]
self._t_shift = value > SAMPLE_PER_SECOND_8
@property
def temperature_rate(self) -> str:
"""
+--------------------------------+--------------------------+
| Mode | Value |
+================================+==========================+
| :py:const:`dps310.RATE_1_HZ` | :py:const:`0b000` |
+--------------------------------+--------------------------+
| :py:const:`dps310.RATE_2_HZ` | :py:const:`0b001` |
+--------------------------------+--------------------------+
| :py:const:`dps310.RATE_4_HZ` | :py:const:`0b010` |
+--------------------------------+--------------------------+
| :py:const:`dps310.RATE_8_HZ` | :py:const:`0b011` |
+--------------------------------+--------------------------+
| :py:const:`dps310.RATE_16_HZ` | :py:const:`0b100` |
+--------------------------------+--------------------------+
| :py:const:`dps310.RATE_32_HZ` | :py:const:`0b101` |
+--------------------------------+--------------------------+
| :py:const:`dps310.RATE_64_HZ` | :py:const:`0b110` |
+--------------------------------+--------------------------+
| :py:const:`dps310.RATE_128_HZ` | :py:const:`0b111` |
+--------------------------------+--------------------------+
"""
values = (
"RATE_1_HZ",
"RATE_2_HZ",
"RATE_4_HZ",
"RATE_8_HZ",
"RATE_16_HZ",
"RATE_32_HZ",
"RATE_64_HZ",
"RATE_128_HZ",
)
return values[self._temperature_rate]
@temperature_rate.setter
def temperature_rate(self, value: int) -> None:
if value not in rates_values:
raise ValueError("Value must be a valid rate setting")
self._temperature_rate = value
@property
def mode(self) -> str:
"""
+------------------------------------+------------------------------------------------------------------+
| Mode | Description |
+------------------------------------+------------------------------------------------------------------+
| :py:const:`dps310.IDLE` | Puts the sensor into a shutdown state |
+------------------------------------+------------------------------------------------------------------+
| :py:const:`dps310.ONE_PRESSURE` | Setting `mode` to ``dps310.ONE_PRESSURE`` takes a single pressure|
| | measurement then switches to ``dps310.IDLE`` |
+------------------------------------+------------------------------------------------------------------+
| :py:const:`dps310.ONE_TEMPERATURE` | Setting `mode` to ``dps310.ONE_TEMPERATURE`` takes a single |
| | temperature measurement then switches to ``dps310.IDLE`` |
+------------------------------------+------------------------------------------------------------------+
| :py:const:`dps310.CONT_PRESSURE` | Take pressure measurements at the current `pressure_rate`. |
| | :attr:`temperature` will not be updated |
+------------------------------------+------------------------------------------------------------------+
| :py:const:`dps310.CONT_TEMP` | Take temperature measurements at the current `temperature_rate`. |
| | :attr:`pressure` will not be updated |
+------------------------------------+------------------------------------------------------------------+
| :py:const:`dps310.CONT_PRESTEMP` | Take temperature and pressure measurements at the current |
| | `pressure_rate` and `temperature_rate` |
+------------------------------------+------------------------------------------------------------------+
"""
values = (
"IDLE",
"ONE_PRESSURE",
"ONE_TEMPERATURE",
"CONT_PRESSURE",
"CONT_TEMP",
"CONT_PRESTEMP",
)
return values[self._sensor_mode]
@mode.setter
def mode(self, value: int) -> None:
self._sensor_mode = value
def _wait_pressure_ready(self) -> None:
"""Wait until a pressure measurement is available. To avoid waiting indefinitely
this function raises an error if the sensor isn't configured for pressure measurements,
"""
if self.mode in (IDLE, ONE_TEMPERATURE, CONT_TEMP):
raise RuntimeError(
"Sensor mode is set to idle or temperature measurement, can't wait for a pressure measurement"
)
while self._pressure_ready is False:
time.sleep(0.001)
def _wait_temperature_ready(self) -> None:
"""Wait until a temperature measurement is available.
To avoid waiting indefinitely this function raises an
error if the sensor isn't configured for temperate measurements,
"""
if self.mode in (IDLE, ONE_PRESSURE, CONT_PRESSURE):
raise RuntimeError(
"Sensor mode is set to idle or pressure measurement, can't wait for a temperature measurement"
)
while self._temp_ready is False:
time.sleep(0.001)
def _read_calibration(self) -> None:
"""
Read the calibration data from the sensor
"""
while not self._coefficients_ready:
time.sleep(0.001)
coeffs = [None] * 18
for offset in range(18):
register = 0x10 + offset
coeffs[offset] = struct.unpack(
"B", self._i2c.readfrom_mem(self._address, register, 1)
)[0]
self._c0 = (coeffs[0] << 4) | ((coeffs[1] >> 4) & 0x0F)
self._c0 = self._twos_complement(self._c0, 12)
self._c1 = self._twos_complement(((coeffs[1] & 0x0F) << 8) | coeffs[2], 12)
self._c00 = (coeffs[3] << 12) | (coeffs[4] << 4) | ((coeffs[5] >> 4) & 0x0F)
self._c00 = self._twos_complement(self._c00, 20)
self._c10 = ((coeffs[5] & 0x0F) << 16) | (coeffs[6] << 8) | coeffs[7]
self._c10 = self._twos_complement(self._c10, 20)
self._c01 = self._twos_complement((coeffs[8] << 8) | coeffs[9], 16)
self._c11 = self._twos_complement((coeffs[10] << 8) | coeffs[11], 16)
self._c20 = self._twos_complement((coeffs[12] << 8) | coeffs[13], 16)
self._c21 = self._twos_complement((coeffs[14] << 8) | coeffs[15], 16)
self._c30 = self._twos_complement((coeffs[16] << 8) | coeffs[17], 16)
@staticmethod
def _twos_complement(val: int, bits: int) -> int:
if val & (1 << (bits - 1)):
val -= 1 << bits
return val
def _correct_temp(self) -> None:
"""Correct temperature readings on ICs with a fuse bit problem"""
self._reg0e = 0xA5
self._reg0f = 0x96
self._reg62 = 0x02
self._reg0e = 0
self._reg0f = 0
_unused = self._raw_temperature
@property
def pressure(self) -> float:
"""Returns the current pressure reading in hectoPascals (hPa)"""
temp_reading = self._raw_temperature
raw_temperature = self._twos_complement(temp_reading, 24)
pressure_reading = self._raw_pressure
raw_pressure = self._twos_complement(pressure_reading, 24)
scaled_rawtemp = raw_temperature / self._temp_scale
scaled_rawpres = raw_pressure / self._pressure_scale
pres_calc = (
self._c00
+ scaled_rawpres
* (self._c10 + scaled_rawpres * (self._c20 + scaled_rawpres * self._c30))
+ scaled_rawtemp
* (self._c01 + scaled_rawpres * (self._c11 + scaled_rawpres * self._c21))
)
final_pressure = pres_calc / 100
return final_pressure
@property
def altitude(self) -> float:
"""
The altitude in meters based on the sea level pressure
(:attr:`sea_level_pressure`) - which you must enter ahead of time
"""
return 44330.0 * (
1.0 - math.pow(self.pressure / self._sea_level_pressure, 0.1903)
)
@altitude.setter
def altitude(self, value: float) -> None:
self.sea_level_pressure = self.pressure / (1.0 - value / 44330.0) ** 5.255
@property
def temperature(self) -> float:
"""The current temperature reading in Celsius"""
scaled_rawtemp = self._raw_temperature / self._temp_scale
temp = scaled_rawtemp * self._c1 + self._c0 / 2.0
return temp
@property
def sea_level_pressure(self) -> float:
"""The local sea level pressure in hectoPascals (aka millibars). This is used
for calculation of :attr:`altitude`. Values are typically in the range
980 - 1030."""
return self._sea_level_pressure
@sea_level_pressure.setter
def sea_level_pressure(self, value: float) -> None:
self._sea_level_pressure = value