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.
 
 
 
 
 
 

528 lines
16 KiB

// -----------------------------------------------------------------------------
// ADE7953 Sensor over I2C
//
// Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
// Copyright (C) 2019 by Antonio López <tonilopezmr at gmail dot com>
// Copyright (C) 2020-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
//
// -----------------------------------------------------------------------------
#if SENSOR_SUPPORT && ADE7953_SUPPORT
#pragma once
#include "BaseEmonSensor.h"
#include "I2CSensor.h"
#include <bitset>
// Original PR based on Tasmota driver
// - https://github.com/arendst/Sonoff-Tasmota/blob/development/sonoff/xnrg_07_ade7953.ino
//
// Device information and data sheet
// - https://www.analog.com/en/products/ade7953.html
// - https://www.analog.com/media/en/technical-documentation/data-sheets/ADE7953.pdf
// Application notes & calibration info
// - https://www.analog.com/media/en/technical-documentation/application-notes/AN-1118.pdf
// Per application notes, energy register accumulates during the line-cycle
// The specific value of watt/h can be calculated as
//
// (Load (in W) * Accumulation time (in sec))
// Wh (with LSB) = ------------------------------------------
// AENERGY{A,B} * 3600s/h
//
// For example, with actual values it means
//
// (220V * 10A * cos(0) * 1sec)
// Wh (with LSB) = ----------------------------
// 20398 * 3600
//
// Assuming the angle between current and voltage is 0 degrees, so we just need to know the accumulation time
// based on Line cycle register and frequency (but, it is calculated and stored in registers ANGLE_A{0x10c} and ANGLE_B{0x10d})
//
// Existing calculation was adjusted to use both actual and expected values.
//
// From the energy registers example (pg. 11)
// > Accumulation time = (0.5 * (1/50) * 100)
// - divide by 2 b/c half-line cycle value of 100 (XXX i.e. number of full cycles?)
// - convert frequency back into seconds
class ADE7953Sensor : public BaseEmonSensor {
private:
static constexpr uint8_t Address { ADE7953_ADDRESS };
static constexpr float LineCycles { ADE7953_LINE_CYCLES };
// No-load threshold (20mA), ignore all readings when current is below this value
// (TODO: pg. 40 "NO-LOAD DETECTION" and {AP,VAR,VA}_NOLOAD registers, implement in config())
static constexpr uint32_t CurrentThreshold { ADE7953_CURRENT_THRESHOLD };
struct Reading {
struct Channel {
float current;
float apparent_power;
float active_power;
float reactive_power;
float active_energy;
};
float frequency;
float voltage;
Channel a;
Channel b;
};
struct Register {
explicit Register(uint16_t address) :
_address(address),
_size(size(address))
{}
uint16_t address() const {
return _address;
}
uint8_t size() const {
return _size;
}
// note that we receive and send MSB first
// just make sure bytes are as-is, not dependant on target
uint32_t read(uint8_t from) const {
return read(from, _address, _size);
}
void write(uint8_t to, uint32_t value) const {
write(to, _address, value, _size);
}
private:
// returns size of the register in *bytes*
static uint8_t size(uint16_t address) {
constexpr uint8_t Lhs { 0b1100 };
constexpr uint8_t Rhs { 0b0011 };
const uint8_t mask = (address >> 8) & 0b1111;
// ref. pg 57, table 11
// > Address Length
// > 0x0ff 8bits (mask)
// > 0x702 8bits Version
// > 0x800 8bits EX_REF
if (!mask || (mask & Lhs)) {
return 1;
// > 0x1ff 16bits (mask)
// > 0x2ff 24bits (mask)
// > 0x3ff 32bits (mask)
} else if (mask & Rhs) {
return mask + 1;
}
return 0;
}
// ref. pg 60
// > The 24-bit register option increases communication speed; the 32-bit register
// > option provides simplicity when coding with the long format.
// > When accessing the 32-bit registers, only the lower 24 bits contain valid
// > data (the upper 8 bits are sign extended)
static uint32_t read(uint8_t address, uint16_t reg, uint8_t size) {
return i2c_read_uint(address, reg, size, false);
}
static void write(uint8_t address, uint16_t reg, uint32_t input, size_t size) {
// Bus-free time minimum 4.7us
i2c_write_uint(address, reg, input, size);
delayMicroseconds(5);
}
uint16_t _address;
uint8_t _size;
};
// ref. pg 65, table 21
// current use-case is finding out whether the active and reactive powers
// have positive or negative sign for the register value
// notice that bits [16:21] have no-load flags, which may be useful when configured
struct ChannelA {
Register current { 0x31a }; // IRMSA
Register apparent_power { 0x310 }; // AVA
Register active_power { 0x312 }; // AWATT
Register reactive_power { 0x314 }; // AVAR
Register active_energy { 0x31e }; // AENERGYA
};
struct ChannelB {
Register current { 0x31b }; // IRMSB
Register apparent_power { 0x311 }; // BVA
Register active_power { 0x313 }; // BWATT
Register reactive_power { 0x315 }; // BVAR
Register active_energy { 0x31f }; // AENERGYB
};
struct AccModeWrapper {
static constexpr size_t Size { 32 };
explicit AccModeWrapper(uint32_t value) :
_value(value)
{}
bool activePowerNegative(ChannelA) const {
return _value[10]; // APSIGN_A
}
bool activePowerNegative(ChannelB) const {
return _value[11]; // APSIGN_B
}
bool reactivePowerNegative(ChannelA) const {
return _value[12]; // VARSIGN_A
}
bool reactivePowerNegative(ChannelB) const {
return _value[13]; // VARSIGN_B
}
private:
std::bitset<Size> _value;
};
class I2CPort {
public:
struct Reading {
struct Channel {
uint32_t current_rms;
uint32_t apparent_power;
uint32_t active_power;
uint32_t reactive_power;
uint32_t active_energy;
};
uint32_t period;
uint32_t voltage_rms;
Channel a;
Channel b;
};
I2CPort() = default;
explicit I2CPort(uint8_t address) :
_address(address)
{}
bool lock(uint8_t address) {
return _sensor_address.findAndLock(address);
}
bool lock() {
return lock(_address);
}
void unlock() {
_sensor_address.unlock();
}
uint8_t address() const {
return _sensor_address.address();
}
// at least for right now, just ignore negative readings
template <typename ChannelRegisters>
Reading::Channel channelRead(AccModeWrapper mode, ChannelRegisters registers) const {
auto current_rms = registers.current.read(_address);
Reading::Channel out{};
if (current_rms > CurrentThreshold) {
out.current_rms = current_rms;
out.active_power = (!mode.activePowerNegative(registers))
? registers.active_power.read(_address) : 0;
out.reactive_power = (!mode.reactivePowerNegative(registers))
? registers.reactive_power.read(_address) : 0;
out.apparent_power = registers.apparent_power.read(_address);
out.active_energy = registers.active_energy.read(_address);
}
return out;
}
Reading read() const {
Reading out{};
const Register Voltage { 0x31c };
out.voltage_rms = Voltage.read(_address);
const Register Period { 0x10e };
out.period = Period.read(_address);
const Register AccMode { 0x301 };
const AccModeWrapper mode { AccMode.read(_address) };
out.a = channelRead(mode, ChannelA{});
out.b = channelRead(mode, ChannelB{});
return out;
}
I2CSensorAddress _sensor_address;
uint8_t _address { 0x00 };
};
Reading read() const {
Reading out{};
auto raw = _port.read();
if (raw.voltage_rms) {
const float voltage = static_cast<double>(raw.voltage_rms) / _voltage_ratio;
// frequency calculation and line period is taken straight from the data sheet (see pg 36):
// > T line = (PERIOD[15:0] + 1) / 223.75kHz
// so, with 50Hz we have 4475 and 60Hz it's 3729. converting straight into Hz
constexpr float PeriodResolution { 223750.0f };
const float frequency = (PeriodResolution / (static_cast<float>(raw.period) + 1.0f));
const auto processChannel = [&](const I2CPort::Reading::Channel& channel) {
Reading::Channel out{};
out.current = static_cast<double>(channel.current_rms) / (_current_ratio * 10.0);
out.active_power = static_cast<double>(channel.active_power) / (_power_active_ratio / 10.0);
if (channel.active_energy) {
out.active_energy = (voltage * out.current * (_line_cycles * (1.0f / frequency))) / static_cast<float>(channel.active_energy);
}
return out;
};
out.voltage = voltage;
out.frequency = frequency;
out.a = processChannel(raw.a);
out.b = processChannel(raw.b);
}
return out;
}
void config(uint8_t address) {
// Need at least 100mS to init ADE7953.
espurna::time::blockingDelay(
espurna::duration::Milliseconds(100));
// Locking the communication interface (Clear bit COMM_LOCK), Enable HPF
const Register Config { 0x102 };
Config.write(address, 0x0004);
// > To modify this (Reserved) register, it must be unlocked by setting
// > Register Address 0xFE to 0xAD immediately prior.
const Register Unlock { 0xfe };
Unlock.write(address, 0xad);
// > This register should be set to 30h to meet the performance specified in Table 1
const Register Reserved { 0x120 };
Reserved.write(address, 0x30);
// > The number of half line cycles written to the LINECYC register
// > is used for both the Current Channel A and Current Channel B
// > accumulation periods
//
// > For example, if a LINECYC value of 100 half line cycles is set
// > and the frequency of the input signal is 50 Hz, the accumulation
// > time is 1 second (0.5 × (1/50) × 100).
const Register HalfLineCycles { 0x101 };
_line_cycles = static_cast<float>(HalfLineCycles.read(address)) / 2.0f;
}
public:
// ---------------------------------------------------------------------
// Sensors API
// ---------------------------------------------------------------------
static constexpr Magnitude Magnitudes[] {
// Common
MAGNITUDE_VOLTAGE,
MAGNITUDE_FREQUENCY,
// Channel A
MAGNITUDE_CURRENT,
MAGNITUDE_POWER_ACTIVE,
MAGNITUDE_POWER_REACTIVE,
MAGNITUDE_POWER_APPARENT,
MAGNITUDE_ENERGY_DELTA,
MAGNITUDE_ENERGY,
// Channel B
MAGNITUDE_CURRENT,
MAGNITUDE_POWER_ACTIVE,
MAGNITUDE_POWER_REACTIVE,
MAGNITUDE_POWER_APPARENT,
MAGNITUDE_ENERGY_DELTA,
MAGNITUDE_ENERGY
};
unsigned char id() const override {
return SENSOR_ADE7953_ID;
}
unsigned char count() const override {
return std::size(Magnitudes);
}
// Initialization method, must be idempotent
void begin() override {
if (!_dirty) {
return;
}
if (!_port.lock(_address)) {
_error = SENSOR_ERROR_I2C;
return;
}
config(_port.address());
_ready = true;
_dirty = false;
}
// Descriptive name of the sensor
String description() const override {
char buffer[25];
snprintf_P(buffer, sizeof(buffer),
PSTR("ADE7953 @ I2C (0x%02X)"), _port.address());
return String(buffer);
}
// Address of the sensor (it could be the GPIO or I2C address)
String address(unsigned char) const override {
char buffer[5];
snprintf_P(buffer, sizeof(buffer),
PSTR("0x%02X"), _port.address());
return String(buffer);
}
// Pre-read hook (usually to populate registers with up-to-date data)
void pre() override {
_last_reading = read();
_energy[0] += espurna::sensor::WattSeconds(_last_reading.a.active_energy);
_energy[1] += espurna::sensor::WattSeconds(_last_reading.b.active_energy);
}
// Sensor has a fixed number of channels, so just use the static magnitudes list
ADE7953Sensor() :
BaseEmonSensor(Magnitudes)
{}
// Current value for slot # index
double value(unsigned char index) override {
switch (index) {
case 0:
return _last_reading.voltage;
case 1:
return _last_reading.frequency;
case 2:
return _last_reading.a.current;
case 3:
return _last_reading.a.active_power;
case 4:
return _last_reading.a.reactive_power;
case 5:
return _last_reading.a.apparent_power;
case 6:
return _last_reading.a.active_energy;
case 7:
return _energy[0].asDouble();
case 8:
return _last_reading.b.current;
case 9:
return _last_reading.b.active_power;
case 10:
return _last_reading.b.reactive_power;
case 11:
return _last_reading.b.apparent_power;
case 12:
return _last_reading.b.active_energy;
case 13:
return _energy[1].asDouble();
}
return 0;
}
// Type for slot # index
unsigned char type(unsigned char index) const override {
if (index < std::size(Magnitudes)) {
return Magnitudes[index].type;
}
return MAGNITUDE_NONE;
}
static constexpr double Iref { 10000.0 };
static constexpr double Uref { 26000.0 };
static constexpr double Pref { 1540.0 };
double defaultRatio(unsigned char index) const override {
switch (index) {
case 0:
return Uref;
case 2:
case 8:
return Iref;
case 3:
case 9:
return Pref;
}
return BaseEmonSensor::defaultRatio(index);
}
double getRatio(unsigned char index) const override {
switch (index) {
case 2:
return _current_ratio_a;
case 8:
return _current_ratio_b;
}
return BaseEmonSensor::getRatio(index);
}
void setRatio(unsigned char index, double value) override {
switch (index) {
case 0:
_voltage_ratio = value;
break;
case 2:
_current_ratio_a = value;
break;
case 3:
_power_ratio_a = value;
break;
case 8:
_current_ratio_b = value;
break;
case 9:
_power_ratio_b = value;
break;
}
}
void setAddress(uint8_t address) {
if (address != _address) {
_dirty = true;
_address = address;
}
}
private:
double _current_ratio_a { Iref };
double _power_ratio_a { Pref };
double _current_ratio_b { Iref };
double _power_ratio_b { Pref };
I2CPort _port;
uint8_t _address { Address };
float _line_cycles { LineCycles };
Reading _last_reading;
};
#if __cplusplus < 201703L
constexpr BaseSensor::Magnitude ADE7953Sensor::Magnitudes[];
#endif
#endif // SENSOR_SUPPORT && ADE7953_SUPPORT