Mirror of espurna firmware for wireless switches and more
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

598 lines
18 KiB

/*
MIT License
Copyright (c) 2020 Wolfgang (Wolle) Ewald
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Originally written by Wolfgang (Wolle) Ewald
- https://github.com/wollewald/INA219_WE (source code)
- https://wolles-elektronikkiste.de/en/ina219-current-and-power-sensor (English)
- https://wolles-elektronikkiste.de/ina219 (German)
Modified by Hamed Taheri for ESPurna (https://hamed-taheri.com)
Modified by Maxim Prokhorov for ESPurna (prokhorov.max@outlook.com)
*/
#pragma once
#include "BaseEmonSensor.h"
#include "I2CSensor.h"
static constexpr uint8_t INA219_CONF_REG { 0x00 }; // Configuration Register
static constexpr uint8_t INA219_SHUNT_REG { 0x01 }; // Shunt Voltage Register
static constexpr uint8_t INA219_BUS_REG { 0x02 }; // Bus Voltage Register
static constexpr uint8_t INA219_PWR_REG { 0x03 }; // Power Register
static constexpr uint8_t INA219_CURRENT_REG { 0x04 }; // Current flowing through Shunt
static constexpr uint8_t INA219_CAL_REG { 0x05 }; // Calibration Register
static constexpr uint16_t INA219_RST { 0x8000 };
class INA219Sensor : public I2CSensor<BaseEmonSensor> {
protected:
using BaseEmonSensor::_current_ratio;
using BaseEmonSensor::_power_active_ratio;
private:
// CONF register bits [0...2] are operating mode
static constexpr uint16_t OperatingModeMask { 0b111 };
// ADC resolution / averaging mode
// bits [3...6] is bus, bits [7...8] is shunt
static constexpr uint16_t AdcModeMask { (0b1111 << 7) | (0b1111 << 3) };
// bits [11...12] configure PGA gain and range (shunt voltage only)
static constexpr uint16_t GainMask { (1 << 12) | (1 << 11) };
// bit 13 is either 16V (0) or 32V (1)
static constexpr uint16_t BusRangeMask { (1 << 13) };
// bit 14 is unused
// 8.6.2.1 Setting this bit to '1' generates a system reset that is the same as power-on reset.
// Resets all registers to *default values*; this bit self-clears.
static constexpr uint16_t ResetMask { (1 << 15) };
public:
enum OperatingMode : uint8_t {
POWER_DOWN = 0,
SHUNT_VOLTAGE_TRIGGERED = 0b1,
BUS_VOLTAGE_TRIGGERED = 0b10,
SHUNT_AND_BUS_TRIGGERED = 0b11,
ADC_OFF = 0b100,
SHUNT_VOLTAGE_CONTINUOUS = 0b101,
BUS_VOLTAGE_CONTINUOUS = 0b110,
SHUNT_AND_BUS_CONTINUOUS = 0b111, // (default)
};
enum AdcMode : uint8_t {
// raw readings, no sampling
// fastest conversion time (less than 1 ms)
// 84, 148, 276 and 532 μs respectively
BIT_MODE_9 = 0,
BIT_MODE_10 = 0b01,
BIT_MODE_11 = 0b10,
BIT_MODE_12 = 0b11, // (default)
// N samples averaged together
// conversion time is N times 532 μs
SAMPLE_MODE_2 = 0b1001,
SAMPLE_MODE_4 = 0b1010,
SAMPLE_MODE_8 = 0b1011,
SAMPLE_MODE_16 = 0b1100,
SAMPLE_MODE_32 = 0b1101,
SAMPLE_MODE_64 = 0b1110,
SAMPLE_MODE_128 = 0b1111,
};
enum Gain : uint8_t {
PG_40 = 0, // Gain 1, range ±40 mV
PG_80 = 0b1, // Gain /2, range ±80 mV
PG_160 = 0b10, // Gain /4, range ±160 mV
PG_320 = 0b11, // Gain /8, range ±320 mV (default)
};
enum BusRange : uint8_t {
BRNG_16 = 0, // 16V
BRNG_32 = 1, // 32V (default)
};
private:
// ref. 8.5.1, calibration is calculated using special LSB value:
// - calibration = truncated(0.04096 / (current LSB * shunt R))
// (0.04096 is a fixed value, set by the device)
// - current LSB = is a value between
// (maximum expected current) / 32767
// (maximum expected current) / 4096
// we specify it in A, calculation is in mA
// determines how much A per bit can be encoded, limited to u16 size
// - power LSB = 20 * current LSB
// we specify it in W, calculation is in mW
// - shunt voltage LSB = 0.01 (mV) (device converts this)
// For example, given the conditions
// - load of 10 A
// - V CM of 12 V
// - R shunt of 2 mOhm
// - V shunt FSR 40 mV
// - BRNG = 0 (VBUS range of 16 V)
// Given the configuration
// - configuration 0x019f
// - calibration 0x5000 (20480)
// Readings would be
// - shunt 0x07d0 (2000); LSB 10 µV, meaning 20 mV
// - bus 0x5d98 (2995); LSB 4 mV, reading means 11.98 V
// - current 0x2710 (10000); LSB 1 mA, reading means 10.0 A
// - power 0x1766 (5990); LSB 20 mW, reading means 119.8 W
// (matching with current multiplied by bus reading)
struct Calibration {
double current_lsb;
double power_lsb;
uint16_t value;
};
struct Lsb {
double min;
double max;
};
// RAW bus voltage is in mV, fixed value LSB is 4mV
static constexpr double BusVoltageLsb { 0.004 };
static constexpr double maxShuntVoltage(Gain gain) {
return (gain == PG_40) ? 0.04 :
(gain == PG_80) ? 0.08 :
(gain == PG_160) ? 0.16 :
(gain == PG_320) ? 0.32 :
0.0;
}
static constexpr double maxPossibleCurrent(Gain gain, double shunt_resistance) {
return (maxShuntVoltage(gain) > 0.0)
? (maxShuntVoltage(gain) / shunt_resistance)
: 0.0;
}
static constexpr Lsb currentLsbRange(double max_expected_current) {
return {
.min = (max_expected_current / 32767.0), // amperes per bit, 15bit resolution
.max = (max_expected_current / 4096.0), // same but 12bit
};
}
static constexpr double currentLsb(double value, double max_expected_current) {
return std::clamp(value,
currentLsbRange(max_expected_current).min,
currentLsbRange(max_expected_current).max);
}
static constexpr double powerLsb(double current_lsb) {
return 20.0 * current_lsb;
}
static
#if __cplusplus > 201703L
constexpr
#endif
uint16_t calibration(double current_lsb, double shunt_resistance) {
constexpr double Min = std::numeric_limits<uint16_t>::min();
constexpr double Max = std::numeric_limits<uint16_t>::max();
return std::clamp(std::trunc(0.04096 / (current_lsb * shunt_resistance)), Min, Max);
}
struct I2CPort {
private:
uint8_t _address { 0 };
public:
struct BusVoltage {
int16_t value;
bool ready;
bool overflow;
};
struct Configuration {
OperatingMode mode;
AdcMode bus_mode;
AdcMode shunt_mode;
Gain gain;
BusRange bus_range;
};
uint8_t address() const {
return _address;
}
void address(uint8_t value) {
_address = value;
}
uint8_t writeRegister(uint8_t reg, uint16_t val) const {
return i2c_write_uint16(_address, reg, val);
}
uint16_t readRegister(uint8_t reg) const {
return i2c_read_uint16(_address, reg);
}
// 8.6.2.1 Setting this bit to '1' generates a system reset that is the same as power-on reset.
// Resets all registers to default values; this bit self-clears.
bool reset() const {
return 0 == writeRegister(INA219_CONF_REG, ResetMask);
}
uint16_t configuration() const {
return readRegister(INA219_CONF_REG);
}
void configuration(uint16_t value) const {
writeRegister(INA219_CONF_REG, value);
}
// Bulk-update for the whole configuration register.
void configuration(Configuration conf) {
auto value = configuration();
value &= ~OperatingModeMask;
value |= conf.mode;
value &= ~AdcModeMask;
value |= (conf.bus_mode << 7) | (conf.shunt_mode << 3);
value &= ~GainMask;
value |= (conf.gain << 11);
value &= ~BusRangeMask;
value |= (conf.bus_range << 13);
configuration(value);
}
uint16_t calibration() const {
return readRegister(INA219_CAL_REG);
}
void calibration(uint16_t calibration) const {
writeRegister(INA219_CAL_REG, calibration);
}
int16_t shuntVoltage() const {
return readRegister(INA219_SHUNT_REG);
}
BusVoltage busVoltage() const {
const int16_t value = readRegister(INA219_BUS_REG);
return {
.value = static_cast<int16_t>(value >> 3),
.ready = (value & 0b10) != 0,
.overflow = (value & 0b1) != 0,
};
}
int16_t current() const {
return readRegister(INA219_CURRENT_REG);
}
int16_t power() const {
return readRegister(INA219_PWR_REG);
}
// helper function for measurement preparations. currently unused
// notice that reading bus voltage clears CNVR (Conversion Ready) Flag
BusVoltage startSingleMeasurement(espurna::duration::Microseconds timeout) const {
busVoltage();
configuration(configuration());
using TimeSource = espurna::time::SystemClock;
const auto start = TimeSource::now();
BusVoltage out;
out.ready = false;
while (!out.ready && (!timeout.count() || (TimeSource::now() - start < timeout))) {
out = busVoltage();
}
return out;
}
BusVoltage startSingleMeasurement() const {
return startSingleMeasurement(espurna::duration::Microseconds(0));
}
void operatingMode(OperatingMode mode) const {
auto value = readRegister(INA219_CONF_REG);
value &= ~OperatingModeMask;
value |= mode;
writeRegister(INA219_CONF_REG, value);
}
// persist current configuration, disable ADC and power down
uint16_t powerDown() const {
const auto value = readRegister(INA219_CONF_REG);
operatingMode(POWER_DOWN);
return value;
}
// power up and restore previously saved config register
void powerUp(uint16_t conf) const {
writeRegister(INA219_CONF_REG, conf);
espurna::time::critical::delay(
espurna::duration::critical::Microseconds(40));
}
};
using TimeSource = espurna::time::CpuClock;
TimeSource::time_point _energy_last;
bool _energy_ready = false;
I2CPort _port;
OperatingMode _operating_mode;
AdcMode _bus_mode;
AdcMode _shunt_mode;
BusRange _bus_range;
Gain _gain;
Calibration _calibration;
double _shunt_resistance;
double _max_expected_current;
Lsb _current_lsb_range;
double _voltage = 0.0;
double _current = 0.0;
double _power = 0.0;
public:
void setOperatingMode(OperatingMode mode) {
_operating_mode = mode;
}
void setBusMode(AdcMode mode) {
_bus_mode = mode;
}
void setShuntMode(AdcMode mode) {
_shunt_mode = mode;
}
void setBusRange(BusRange range) {
_bus_range = range;
}
void setGain(Gain gain) {
_gain = gain;
}
void setShuntResistance(double value) {
_shunt_resistance = value;
}
void setMaxExpectedCurrent(double current) {
_max_expected_current = current;
_current_lsb_range = currentLsbRange(current);
_current_ratio = currentLsb(_current_lsb_range.min, current);
_power_active_ratio = powerLsb(_current_ratio);
}
static constexpr Magnitude Magnitudes[] {
MAGNITUDE_VOLTAGE,
MAGNITUDE_CURRENT,
MAGNITUDE_POWER_ACTIVE,
MAGNITUDE_ENERGY,
};
// We only allow to adjust values associated with LSB
// In case voltage and energy ratios change something,
// use `Magnitudes` in the methods below
static constexpr Magnitude RatioSupport[] {
MAGNITUDE_NONE,
MAGNITUDE_CURRENT,
MAGNITUDE_POWER_ACTIVE,
MAGNITUDE_NONE,
};
static_assert(std::size(Magnitudes) == std::size(RatioSupport), "");
INA219Sensor() :
I2CSensor<BaseEmonSensor>(Magnitudes)
{}
unsigned char id() const override {
return SENSOR_INA219_ID;
}
unsigned char count() const override {
return std::size(Magnitudes);
}
signed char decimals(espurna::sensor::Unit) const override {
return 2;
}
void begin() override {
auto max_possible_current = maxPossibleCurrent(_gain, _shunt_resistance);
if (max_possible_current < _max_expected_current) {
_error = SENSOR_ERROR_CONFIG;
return;
}
static constexpr uint8_t addresses[] { 0x40, 0x41, 0x44, 0x45 };
const auto address = findAndLock(addresses);
if (address == 0) {
return;
}
_port.address(address);
if (!_port.reset()) {
_error = SENSOR_ERROR_NOT_READY;
return;
}
_port.configuration({
.mode = _operating_mode,
.bus_mode = _bus_mode,
.shunt_mode = _shunt_mode,
.gain = _gain,
.bus_range = _bus_range,
});
#if SENSOR_DEBUG
DEBUG_MSG(PSTR("[INA219] Maximum possible current %sA\n"),
String(max_possible_current, 3).c_str());
DEBUG_MSG(PSTR("[INA219] Current LSB Range %s...%s\n"),
String(_current_lsb_range.min, 6).c_str(),
String(_current_lsb_range.max, 6).c_str());
#endif
_ratios_changed = true;
_energy_ready = false;
_error = SENSOR_ERROR_OK;
_ready = true;
}
String address(unsigned char index) const override {
return String(_port.address(), 10);
}
String description() const override {
char buffer[32];
snprintf_P(buffer, sizeof(buffer),
PSTR("INA219 @ I2C (0x%02X)"), _port.address());
return String(buffer);
}
unsigned char type(unsigned char index) const override {
if (index < std::size(Magnitudes)) {
return Magnitudes[index].type;
}
return MAGNITUDE_NONE;
}
void tick() override {
if (_ratios_changed) {
_calibration = Calibration{
.current_lsb = _current_ratio,
.power_lsb = _power_active_ratio,
.value = calibration(_current_ratio, _shunt_resistance),
};
#if SENSOR_DEBUG
DEBUG_MSG(PSTR("[INA219] Current LSB %s\n"),
String(_calibration.current_lsb, 6).c_str());
DEBUG_MSG(PSTR("[INA219] Power LSB %s\n"),
String(_calibration.power_lsb, 6).c_str());
uint8_t buf[2];
std::memcpy(std::begin(buf), &_calibration.value, sizeof(buf));
DEBUG_MSG(PSTR("[INA219] Calibration 0x%s\n"),
hexEncode(std::begin(buf), std::end(buf)).c_str());
#endif
_port.calibration(_calibration.value);
espurna::time::blockingDelay(
espurna::duration::Milliseconds(100));
_ratios_changed = false;
}
}
void pre() override {
_error = SENSOR_ERROR_OK;
const auto voltage = _port.busVoltage();
if (!voltage.ready) {
_error = SENSOR_ERROR_NOT_READY;
return;
}
if (voltage.overflow) {
_error = SENSOR_ERROR_OVERFLOW;
return;
}
_voltage = voltage.value * BusVoltageLsb;
_current = _port.current() * _calibration.current_lsb;
_power = _port.power() * _calibration.power_lsb;
const auto now = TimeSource::now();
if (_energy_ready) {
using namespace espurna::sensor;
const auto elapsed = std::chrono::duration_cast<espurna::duration::Seconds>(now - _energy_last);
_energy[0] += WattSeconds(Watts{_power}, elapsed);
}
_energy_last = now;
_energy_ready = true;
}
double value(unsigned char index) override {
if (index < std::size(Magnitudes)) {
switch (Magnitudes[index].type) {
case MAGNITUDE_VOLTAGE:
return _voltage;
case MAGNITUDE_CURRENT:
return _current;
case MAGNITUDE_POWER_ACTIVE:
return _power;
case MAGNITUDE_ENERGY:
return _energy[0].asDouble();
}
}
return 0;
}
using BaseEmonSensor::simpleGetRatio;
double getRatio(unsigned char index) const override {
return simpleGetRatio(RatioSupport, index);
}
using BaseEmonSensor::simpleSetRatio;
void setRatio(unsigned char index, double value) override {
simpleSetRatio(RatioSupport, index, value);
}
double defaultRatio(unsigned char index) const override {
if (index < std::size(Magnitudes)) {
switch (Magnitudes[index].type) {
case MAGNITUDE_CURRENT:
return _current_lsb_range.min;
case MAGNITUDE_POWER_ACTIVE:
return powerLsb(_current_lsb_range.min);
}
}
return BaseEmonSensor::defaultRatio(index);
}
};
#if __cplusplus < 201703L
constexpr BaseSensor::Magnitude INA219Sensor::Magnitudes[];
constexpr BaseSensor::Magnitude INA219Sensor::RatioSupport[];
#endif