From 13bfb6676f310a3a505c5051e66f90c4d12e2024 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 1 May 2026 15:36:27 +0200 Subject: [PATCH] adding to local git repo --- .gitignore | 4 + README.md | 0 ac_voltage_sensor/__init__.py | 0 ac_voltage_sensor/ac_voltage_sensor.cpp | 73 + ac_voltage_sensor/ac_voltage_sensor.h | 52 + ac_voltage_sensor/sensor.py | 45 + ads1115_int/__init__.py | 42 + ads1115_int/ads1115_int.cpp | 317 ++++ ads1115_int/ads1115_int.h | 93 ++ ads1115_int/sensor/__init__.py | 98 ++ ads1115_int/sensor/ads1115_int_sensor.cpp | 28 + ads1115_int/sensor/ads1115_int_sensor.h | 37 + ads1115_pol/__init__.py | 32 + ads1115_pol/ads1115_pol.cpp | 223 +++ ads1115_pol/ads1115_pol.h | 64 + ads1115_pol/sensor/__init__.py | 98 ++ ads1115_pol/sensor/ads1115_pol_sensor.cpp | 36 + ads1115_pol/sensor/ads1115_pol_sensor.h | 38 + ads131m08/README.md | 80 + ads131m08/__init__.py | 80 + ads131m08/ads131m08.cpp | 1720 +++++++++++++++++++++ ads131m08/ads131m08.h | 196 +++ ads131m08/ads131m08_defs.h | 358 +++++ ads131m08/git_errors.txt | 104 ++ ads131m08/sensor/__init__.py | 91 ++ ads131m08/sensor/ads131m08_sensor.cpp | 90 ++ ads131m08/sensor/ads131m08_sensor.h | 63 + ads131m08/sensor_rms/__init__.py | 75 + ads131m08/uint_str.h | 28 + canbus/__init__.py | 183 +++ canbus/canbus.cpp | 106 ++ canbus/canbus.h | 184 +++ ds3231/__init__.py | 0 ds3231/ds3231.cpp | 112 ++ ds3231/ds3231.h | 70 + ds3231/time.py | 58 + mcp2515/mcp2515.cpp | 711 +++++++++ mcp2515/mcp2515.h | 112 ++ mcp2515/mcp2515_defs.h | 375 +++++ rtq6056/__init__.py | 1 + rtq6056/rtq6056.cpp | 167 ++ rtq6056/rtq6056.h | 78 + rtq6056/sensor.py | 147 ++ tlc59208f_ext/__init__.py | 32 + tlc59208f_ext/output.py | 27 + tlc59208f_ext/tlc59208f_output.cpp | 211 +++ tlc59208f_ext/tlc59208f_output.h | 79 + 47 files changed, 6818 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 ac_voltage_sensor/__init__.py create mode 100644 ac_voltage_sensor/ac_voltage_sensor.cpp create mode 100644 ac_voltage_sensor/ac_voltage_sensor.h create mode 100644 ac_voltage_sensor/sensor.py create mode 100644 ads1115_int/__init__.py create mode 100644 ads1115_int/ads1115_int.cpp create mode 100644 ads1115_int/ads1115_int.h create mode 100644 ads1115_int/sensor/__init__.py create mode 100644 ads1115_int/sensor/ads1115_int_sensor.cpp create mode 100644 ads1115_int/sensor/ads1115_int_sensor.h create mode 100644 ads1115_pol/__init__.py create mode 100644 ads1115_pol/ads1115_pol.cpp create mode 100644 ads1115_pol/ads1115_pol.h create mode 100644 ads1115_pol/sensor/__init__.py create mode 100644 ads1115_pol/sensor/ads1115_pol_sensor.cpp create mode 100644 ads1115_pol/sensor/ads1115_pol_sensor.h create mode 100644 ads131m08/README.md create mode 100644 ads131m08/__init__.py create mode 100644 ads131m08/ads131m08.cpp create mode 100644 ads131m08/ads131m08.h create mode 100644 ads131m08/ads131m08_defs.h create mode 100644 ads131m08/git_errors.txt create mode 100644 ads131m08/sensor/__init__.py create mode 100644 ads131m08/sensor/ads131m08_sensor.cpp create mode 100644 ads131m08/sensor/ads131m08_sensor.h create mode 100644 ads131m08/sensor_rms/__init__.py create mode 100644 ads131m08/uint_str.h create mode 100644 canbus/__init__.py create mode 100644 canbus/canbus.cpp create mode 100644 canbus/canbus.h create mode 100644 ds3231/__init__.py create mode 100644 ds3231/ds3231.cpp create mode 100644 ds3231/ds3231.h create mode 100644 ds3231/time.py create mode 100644 mcp2515/mcp2515.cpp create mode 100644 mcp2515/mcp2515.h create mode 100644 mcp2515/mcp2515_defs.h create mode 100644 rtq6056/__init__.py create mode 100644 rtq6056/rtq6056.cpp create mode 100644 rtq6056/rtq6056.h create mode 100644 rtq6056/sensor.py create mode 100644 tlc59208f_ext/__init__.py create mode 100644 tlc59208f_ext/output.py create mode 100644 tlc59208f_ext/tlc59208f_output.cpp create mode 100644 tlc59208f_ext/tlc59208f_output.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf4923d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__ +__pycache__/* +__pycache__/*.* + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/ac_voltage_sensor/__init__.py b/ac_voltage_sensor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ac_voltage_sensor/ac_voltage_sensor.cpp b/ac_voltage_sensor/ac_voltage_sensor.cpp new file mode 100644 index 0000000..37ff63f --- /dev/null +++ b/ac_voltage_sensor/ac_voltage_sensor.cpp @@ -0,0 +1,73 @@ +#include "ac_voltage_sensor.h" + +#include "esphome/core/log.h" +#include +#include + +namespace esphome { +namespace ac_voltage_sensor { + +static const char *const TAG = "ac_voltage_sensor"; + +void AC_Voltage_Sensor::dump_config() { + LOG_SENSOR("", "AC Voltage Sensor", this); + ESP_LOGCONFIG(TAG, " Sample Duration: %.2fs", this->sample_duration_ / 1e3f); + LOG_UPDATE_INTERVAL(this); +} + +void AC_Voltage_Sensor::update() { + // Update only starts the sampling phase, in loop() the actual sampling is happening. + + // Request a high loop() execution interval during sampling phase. + this->high_freq_.start(); + + // Set timeout for ending sampling phase + this->set_timeout("read", this->sample_duration_, [this]() { + this->is_sampling_ = false; + this->high_freq_.stop(); + + if (this->num_samples_ == 0) { + // Shouldn't happen, but let's not crash if it does. + this->publish_state(NAN); + return; + } + + const float rms_ac_dc_squared = this->sample_squared_sum_ / this->num_samples_; + const float rms_dc = this->sample_sum_ / this->num_samples_; + const float rms_ac_squared = rms_ac_dc_squared - rms_dc * rms_dc; + float rms_ac = 0; + if (rms_ac_squared > 0) + rms_ac = std::sqrt(rms_ac_squared); + ESP_LOGD(TAG, "'%s' - Raw AC Value: %.3fV, DC: %.3fV after %" PRIu32 " different samples (%" PRIu32 " SPS)", + this->name_.c_str(), rms_ac, rms_dc, this->num_samples_, 1000 * this->num_samples_ / this->sample_duration_); + this->publish_state(rms_ac); + }); + + // Set sampling values + this->last_value_ = 0.0; + this->num_samples_ = 0; + this->sample_sum_ = 0.0f; + this->sample_squared_sum_ = 0.0f; + this->is_sampling_ = true; +} + +void AC_Voltage_Sensor::loop() { + if (!this->is_sampling_) + return; + + // Perform a single sample + float value = this->source_->sample(); + if (std::isnan(value)) + return; + + // Assuming a sine wave, avoid requesting values faster than the ADC can provide them + if (this->last_value_ == value) + return; + this->last_value_ = value; + this->num_samples_++; + this->sample_sum_ += value; + this->sample_squared_sum_ += value * value; +} + +} // namespace ac_voltage_sensor +} // namespace esphome diff --git a/ac_voltage_sensor/ac_voltage_sensor.h b/ac_voltage_sensor/ac_voltage_sensor.h new file mode 100644 index 0000000..071c019 --- /dev/null +++ b/ac_voltage_sensor/ac_voltage_sensor.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" + +namespace esphome { +namespace ac_voltage_sensor { + +class AC_Voltage_Sensor : public sensor::Sensor, public PollingComponent { + public: + void update() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { + // After the base sensor has been initialized + return setup_priority::DATA - 1.0f; + } + + void set_sample_duration(uint32_t sample_duration) { sample_duration_ = sample_duration; } + void set_source(voltage_sampler::VoltageSampler *source) { source_ = source; } + + protected: + /// High Frequency loop() requester used during sampling phase. + HighFrequencyLoopRequester high_freq_; + + /// Duration in ms of the sampling phase. + uint32_t sample_duration_; + /// The sampling source to read values from. + voltage_sampler::VoltageSampler *source_; + + /** The DC offset of the circuit is removed from the AC value, i.e. the value in dc_component_ is subtracted from the sampled value + * + * Diagram: https://learn.openenergymonitor.org/electricity-monitoring/ct-sensors/interface-with-arduino + * + * The AC component is essentially the same as the calculating the Standard-Deviation, + * which can be done by cumulating 3 values per sample: + * 1) Number of samples + * 2) Sum of samples + * 3) Sum of sample squared + * https://en.wikipedia.org/wiki/Root_mean_square + */ + float last_value_ = 0.0f; + float sample_sum_ = 0.0f; + float sample_squared_sum_ = 0.0f; + uint32_t num_samples_ = 0; + bool is_sampling_ = false; +}; + +} // namespace ac_voltage_sensor +} // namespace esphome diff --git a/ac_voltage_sensor/sensor.py b/ac_voltage_sensor/sensor.py new file mode 100644 index 0000000..54d02a6 --- /dev/null +++ b/ac_voltage_sensor/sensor.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +from esphome.components import sensor, voltage_sampler +import esphome.config_validation as cv +from esphome.const import ( + CONF_SENSOR, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_VOLT, +) +# based on ctclamp by jesserockz +AUTO_LOAD = ["voltage_sampler"] +CODEOWNERS = ["@stuurmcp"] + +CONF_SAMPLE_DURATION = "sample_duration" + +ac_voltage_sensor_ns = cg.esphome_ns.namespace("ac_voltage_sensor") +AC_Voltage_Sensor = ac_voltage_sensor_ns.class_("AC_Voltage_Sensor", sensor.Sensor, cg.PollingComponent) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + AC_Voltage_Sensor, + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.Required(CONF_SENSOR): cv.use_id(voltage_sampler.VoltageSampler), + cv.Optional( + CONF_SAMPLE_DURATION, default="200ms" + ): cv.positive_time_period_milliseconds, + } + ) + .extend(cv.polling_component_schema("60s")) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + + sens = await cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_source(sens)) + cg.add(var.set_sample_duration(config[CONF_SAMPLE_DURATION])) diff --git a/ads1115_int/__init__.py b/ads1115_int/__init__.py new file mode 100644 index 0000000..74b18fd --- /dev/null +++ b/ads1115_int/__init__.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +from esphome.components import i2c +from esphome import pins +import esphome.config_validation as cv +from esphome.const import CONF_ID + +CONF_ALERT_RDY_PIN = "alert_rdy_pin" + +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +ads1115_int_ns = cg.esphome_ns.namespace("ads1115_int") +ADS1115Component = ads1115_int_ns.class_("ADS1115Component", cg.Component, i2c.I2CDevice) + +CONF_CONTINUOUS_MODE = "continuous_mode" +CONF_INTERLEAVED_MODE = "interleaved_mode" +CONF_ADS1115_ID = "ads1115_id" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ADS1115Component), + cv.Required(CONF_ALERT_RDY_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_INTERLEAVED_MODE, default=False): cv.boolean, + cv.Optional(CONF_CONTINUOUS_MODE, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(None)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + alert_rdy = await cg.gpio_pin_expression( + config.get(CONF_ALERT_RDY_PIN) + ) + cg.add(var.set_alert_pin(alert_rdy)) + cg.add(var.set_interleaved_mode(config[CONF_INTERLEAVED_MODE])) + cg.add(var.set_continuous_mode(config[CONF_CONTINUOUS_MODE])) diff --git a/ads1115_int/ads1115_int.cpp b/ads1115_int/ads1115_int.cpp new file mode 100644 index 0000000..0ba04c1 --- /dev/null +++ b/ads1115_int/ads1115_int.cpp @@ -0,0 +1,317 @@ +#include "ads1115_int.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ads1115_int { + +static const char *const TAG = "ads1115_int"; +static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00; +static const uint8_t ADS1115_REGISTER_CONFIG = 0x01; +static const uint8_t ADS1115_REGISTER_LO_TRESH = 0x02; +static const uint8_t ADS1115_REGISTER_HI_TRESH = 0x03; + +void ADS1115Component::setup() +{ + uint16_t value; + if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)) { + this->mark_failed("Could not read ADS1115 conversion register on setup."); + return; + } + // 1. Create a binary semaphore + data_ready_sem = xSemaphoreCreateBinary(); + this->alert_pin_->setup(); + this->alert_pin_->attach_interrupt(&ADS1115Component::isr_handler, this, gpio::INTERRUPT_FALLING_EDGE); + // setup with default values + uint16_t config = 0; + // Clear single-shot bit + // 0b0xxxxxxxxxxxxxxx + config |= 0b0000000000000000; + // Setup multiplexer + // 0bx000xxxxxxxxxxxx + config |= ADS1115_MULTIPLEXER_P0_N1 << 12; + + // Setup Gain + // 0bxxxx000xxxxxxxxx + config |= ADS1115_GAIN_6P144 << 9; + + if(!this->continuous_mode_) { + // Set singleshot mode + // 0bxxxxxxx1xxxxxxxx + config |= 0b0000000100000000; + } + // Set data rate - 860 samples per second + // 0bxxxxxxxx100xxxxx + config |= ADS1115_860SPS << 5; + +// // Set comparator mode - hysteresis +// // 0bxxxxxxxxxxx0xxxx +// config |= 0b0000000000000000; +// +// // Set comparator polarity - active low +// // 0bxxxxxxxxxxxx0xxx +// config |= 0b0000000000000000; +// +// // Set comparator latch enabled - false +// // 0bxxxxxxxxxxxxx0xx +// config |= 0b0000000000000000; +// +// // Set comparator que mode - disabled +// // 0bxxxxxxxxxxxxxx11 +// config |= 0b0000000000000011; + + if (!this->write_byte_16(ADS1115_REGISTER_CONFIG, config)) { + this->mark_failed("ADS1115 config register write failed on setup."); + return; + } + + this->prev_config_ = config; + if(!this->set_data_ready_mode()) { + this->mark_failed("ADS1115 data ready mode setup failed."); + return; + } +} +// ISR function to handle interrupt from alert pin +// MUST be fast and non-blocking +void IRAM_ATTR ADS1115Component::isr_handler(ADS1115Component *arg) +{ + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + xSemaphoreGiveFromISR(arg->data_ready_sem, &xHigherPriorityTaskWoken); + if (xHigherPriorityTaskWoken == pdTRUE) { + portYIELD_FROM_ISR(); // Switch to the waiting task immediately + } +} + +void ADS1115Component::loop() +{ + // Check the semaphore (0 timeout means non-blocking) + if (xSemaphoreTake(data_ready_sem, 0) == pdTRUE) { + // Perform the I2C read here safely outside of ISR + this->read_request_next(); + } +} + +// we read data for the current mux index, then set up the next conversion +void ADS1115Component::read_request_next() +{ + uint16_t config = 0x0000; + // We do not support multiple ads1115 sharing the same alert/rdy pin, however we still read the ads1115 config register to get the multiplexer value. This allows us to also check the operational status (OS) bit. + // If OS bit set, i.e. the conversion is done meaning we can read the data; this is valid only in single shot mode, in continuous mode we can read data without checking the status bit + //ESP_LOGD(TAG, "Config: 0x%04X", this->prev_config_); + //ESP_LOGD(TAG, "I2C bus frequency: %.1f kHz", bus_->bus_frequency_ / 1000.0f); + if(this->read_byte_16(ADS1115_REGISTER_CONFIG, &config)) { + //ESP_LOGD(TAG, "Config register: 0x%04X", config); + int ads_mux = (config >> 12) & 0b111; + bool conversion_done = (config >> 15) == 1 || this->continuous_mode_; + //ESP_LOGD(TAG, "Current MUX setting according to ADS1115: %d", ads_mux); + int ads_mux_index = get_mux_index(static_cast(ads_mux)); + //ESP_LOGD(TAG, "Current MUX index according to ADS1115: %d", ads_mux_index); + if (conversion_done && ads_mux_index != -1) { + if(this->interleaved_mode_) { + // in interleaved mode, we request the next conversion already here + int next_index = next_mux_data_index(ads_mux_index); + if(next_index != -1) { + this->start_conversion(next_index); + } + } + int16_t raw_value = 0; + double v = this->read_data(raw_value, ads_mux_index); + if (!std::isnan(v)) { + auto sensor_ptr = this->muxdata_[ads_mux_index].sensor_ptr; + ESP_LOGV(TAG, "'% -18s': Raw=% 6d %fV", sensor_ptr->get_name().c_str(), raw_value, v); + sensor_ptr->publish_state(v); + } + if(!this->interleaved_mode_) { + // we have read data for the current mux index, move to next + int next_index = next_mux_data_index(ads_mux_index); + if(next_index != -1) { + this->start_conversion(next_index); + } + } + } + } +} + +int ADS1115Component::get_mux_index(ADS1115Multiplexer multiplexer) +{ + for(int i=0; i<4; i++) { + if(this->muxdata_[i].multiplexer == multiplexer) { + return i; + } + } + return -1; // not found +} + +double ADS1115Component::read_data(int16_t& raw_value, int mux_index) +{ + raw_value = 0; + uint16_t raw_conversion; + if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &raw_conversion)) { + this->status_set_warning(); + return NAN; + } + ADS1115Resolution resolution = this->muxdata_[mux_index].resolution; + // Adjust for 12-bit resolution if needed + if (resolution == ADS1015_12_BITS) { + bool negative = (raw_conversion >> 15) == 1; + + // shift raw_conversion as it's only 12-bits, left justified + raw_conversion = raw_conversion >> (16 - ADS1015_12_BITS); + + // check if number was negative in order to keep the sign + if (negative) { + // the number was negative + // 1) set the negative bit back + raw_conversion |= 0x8000; + // 2) reset the former (shifted) negative bit + raw_conversion &= 0xF7FF; + } + } + auto signed_conversion = static_cast(raw_conversion); + raw_value = signed_conversion; + double millivolts; + double divider = (resolution == ADS1115_16_BITS) ? 32768.0f : 2048.0f; + switch (this->muxdata_[mux_index].gain) { + case ADS1115_GAIN_6P144: + millivolts = (signed_conversion * 6144) / divider; + break; + case ADS1115_GAIN_4P096: + millivolts = (signed_conversion * 4096) / divider; + break; + case ADS1115_GAIN_2P048: + millivolts = (signed_conversion * 2048) / divider; + break; + case ADS1115_GAIN_1P024: + millivolts = (signed_conversion * 1024) / divider; + break; + case ADS1115_GAIN_0P512: + millivolts = (signed_conversion * 512) / divider; + break; + case ADS1115_GAIN_0P256: + millivolts = (signed_conversion * 256) / divider; + break; + default: + millivolts = NAN; + } + + this->status_clear_warning(); + return millivolts / 1e3f; +} + +// Method to set mux data for later use, i.e. when data is to be read +int ADS1115Component::set_mux_data(ADS1115Multiplexer multiplexer, ADS1115Gain gain, ADS1115Resolution resolution, ADS1115Samplerate samplerate, sensor::Sensor *sensor_ptr) +{ + int index = -1; + for(int i=0; i<4; i++) { + if(this->muxdata_[i].multiplexer == multiplexer) { + index = i; + break; + } + if(this->muxdata_[i].multiplexer == ADS1115_MULTIPLEXER_INVALID && index == -1) { + index = i; + } + } + if(index != -1) { + this->muxdata_[index].multiplexer = multiplexer; + this->muxdata_[index].gain = gain; + this->muxdata_[index].resolution = resolution; + this->muxdata_[index].samplerate = samplerate; + this->muxdata_[index].sensor_ptr = sensor_ptr; + } + return index; +} + +bool ADS1115Component::set_data_ready_mode() +{ + uint16_t config = this->prev_config_; + // Set comparator que mode - assert after one conversion + // 0bxxxxxxxxxxxxxx00 + config &= 0b1111111111111100; + + if (!this->write_byte_16(ADS1115_REGISTER_CONFIG, config)) { + return false; + } + this->prev_config_ = config; + if(!this->write_byte_16(ADS1115_REGISTER_HI_TRESH, 0x8000)) { + return false; + } + if(!this->write_byte_16(ADS1115_REGISTER_LO_TRESH, 0x0000)) { + return false; + } + return true; +} + +bool ADS1115Component::start_conversion(int mux_index) +{ + auto& muxdata = this->muxdata_[mux_index]; + auto multiplexer = muxdata.multiplexer; + if (multiplexer != ADS1115_MULTIPLEXER_INVALID) { + auto gain = muxdata.gain; + auto resolution = muxdata.resolution; + auto samplerate = muxdata.samplerate; + uint16_t config = this->prev_config_; + // Multiplexer + // 0bxBBBxxxxxxxxxxxx + config &= 0b1000111111111111; + config |= (multiplexer & 0b111) << 12; + + // Gain + // 0bxxxxBBBxxxxxxxxx + config &= 0b1111000111111111; + config |= (gain & 0b111) << 9; + + // Sample rate + // 0bxxxxxxxxBBBxxxxx + config &= 0b1111111100011111; + config |= (samplerate & 0b111) << 5; + + if(!this->continuous_mode_) { + // Set single-shot mode + // 0bxxxxxxx1xxxxxxxx + config |= 0b0000000100000000; + } + else { + // Clear single-shot bit + // 0b0xxxxxxxxxxxxxxx + config |= 0b0000000000000000; + } + // Start conversion; this has no effect in continuous mode + config |= 0b1000000000000000; + + if (!this->write_byte_16(ADS1115_REGISTER_CONFIG, config)) { + this->status_set_warning(); + return false; + } + this->prev_config_ = config; + return true; + } + return false; +} + +int ADS1115Component::next_mux_data_index(int start_index) +{ + int result = start_index; + do { + result = (result + 1) % 4; + if(this->muxdata_[result].multiplexer != ADS1115_MULTIPLEXER_INVALID) { + return result; + } + } while(result != start_index); + return -1; // no valid mux data found +} + +void ADS1115Component::dump_config() +{ + ESP_LOGCONFIG(TAG, "ADS1115_INT:"); + LOG_I2C_DEVICE(this); + ESP_LOGCONFIG(TAG, " Interleaved mode: %s", YESNO(this->interleaved_mode_)); + ESP_LOGCONFIG(TAG, " Continuous mode: %s", YESNO(this->continuous_mode_)); + LOG_PIN(" ALERT Pin:", this->alert_pin_); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} + + +} // namespace ads1115_int +} // namespace esphome diff --git a/ads1115_int/ads1115_int.h b/ads1115_int/ads1115_int.h new file mode 100644 index 0000000..b333c93 --- /dev/null +++ b/ads1115_int/ads1115_int.h @@ -0,0 +1,93 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" // Ensure this header defines i2c::I2CDevice +#include "esphome/core/component.h" // Ensure this header defines Component +#include "esphome/components/sensor/sensor.h" + +#include +#include +#include +#include + +namespace esphome { +namespace ads1115_int { + +enum ADS1115Multiplexer { + ADS1115_MULTIPLEXER_P0_N1 = 0b000, + ADS1115_MULTIPLEXER_P0_N3 = 0b001, + ADS1115_MULTIPLEXER_P1_N3 = 0b010, + ADS1115_MULTIPLEXER_P2_N3 = 0b011, + ADS1115_MULTIPLEXER_P0_NG = 0b100, + ADS1115_MULTIPLEXER_P1_NG = 0b101, + ADS1115_MULTIPLEXER_P2_NG = 0b110, + ADS1115_MULTIPLEXER_P3_NG = 0b111, + ADS1115_MULTIPLEXER_INVALID = 0xFF +}; + +enum ADS1115Gain { + ADS1115_GAIN_6P144 = 0b000, + ADS1115_GAIN_4P096 = 0b001, + ADS1115_GAIN_2P048 = 0b010, + ADS1115_GAIN_1P024 = 0b011, + ADS1115_GAIN_0P512 = 0b100, + ADS1115_GAIN_0P256 = 0b101, +}; + +enum ADS1115Resolution { + ADS1115_16_BITS = 16, + ADS1015_12_BITS = 12, +}; + +enum ADS1115Samplerate { + ADS1115_8SPS = 0b000, + ADS1115_16SPS = 0b001, + ADS1115_32SPS = 0b010, + ADS1115_64SPS = 0b011, + ADS1115_128SPS = 0b100, + ADS1115_250SPS = 0b101, + ADS1115_475SPS = 0b110, + ADS1115_860SPS = 0b111 +}; + +// we will force single-shot mode for interrupt operation +class ADS1115Component : public Component, public i2c::I2CDevice { + private: + struct mux_data_item { + ADS1115Multiplexer multiplexer; + ADS1115Gain gain; + ADS1115Resolution resolution; + ADS1115Samplerate samplerate; + sensor::Sensor *sensor_ptr; + mux_data_item() { multiplexer = ADS1115_MULTIPLEXER_INVALID; } + }; + int mux_data_index_{0}; + mux_data_item muxdata_[4]; + int next_mux_data_index(int start_index); + void read_request_next(); + int get_mux_index(ADS1115Multiplexer multiplexer); + + public: + // Semaphore handle + SemaphoreHandle_t data_ready_sem = NULL; + void setup() override; + void loop() override; + void dump_config() override; + double read_data(int16_t& raw_value, int mux_index); + bool start_conversion(int mux_index); + void set_alert_pin(InternalGPIOPin *pin) { alert_pin_ = pin; } + void set_interleaved_mode(bool interleaved) { this->interleaved_mode_ = interleaved; } + void set_continuous_mode(bool continuous) { this->continuous_mode_ = continuous; } + bool set_data_ready_mode(); + int set_mux_data(ADS1115Multiplexer multiplexer, ADS1115Gain gain, ADS1115Resolution resolution, ADS1115Samplerate samplerate, sensor::Sensor *sensor_ptr); + + protected: + bool interleaved_mode_{false}; + bool continuous_mode_{false}; + uint16_t prev_config_{0}; + InternalGPIOPin *alert_pin_; + static void IRAM_ATTR isr_handler(ADS1115Component *arg); + +}; + +} // namespace ads1115_int +} // namespace esphome diff --git a/ads1115_int/sensor/__init__.py b/ads1115_int/sensor/__init__.py new file mode 100644 index 0000000..e90de1f --- /dev/null +++ b/ads1115_int/sensor/__init__.py @@ -0,0 +1,98 @@ +import esphome.codegen as cg +from esphome.components import sensor #, voltage_sampler +import esphome.config_validation as cv +from esphome.const import ( + CONF_GAIN, + CONF_ID, + CONF_MULTIPLEXER, + CONF_RESOLUTION, + CONF_SAMPLE_RATE, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_VOLT, +) + +from .. import CONF_ADS1115_ID, ADS1115Component, ads1115_int_ns + +#AUTO_LOAD = ["voltage_sampler"] +DEPENDENCIES = ["ads1115_int"] + +ADS1115Multiplexer = ads1115_int_ns.enum("ADS1115Multiplexer") +MUX = { + "A0_A1": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_N1, + "A0_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_N3, + "A1_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P1_N3, + "A2_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P2_N3, + "A0_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_NG, + "A1_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P1_NG, + "A2_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P2_NG, + "A3_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P3_NG, +} + +ADS1115Gain = ads1115_int_ns.enum("ADS1115Gain") +GAIN = { + "6.144": ADS1115Gain.ADS1115_GAIN_6P144, + "4.096": ADS1115Gain.ADS1115_GAIN_4P096, + "2.048": ADS1115Gain.ADS1115_GAIN_2P048, + "1.024": ADS1115Gain.ADS1115_GAIN_1P024, + "0.512": ADS1115Gain.ADS1115_GAIN_0P512, + "0.256": ADS1115Gain.ADS1115_GAIN_0P256, +} + +ADS1115Resolution = ads1115_int_ns.enum("ADS1115Resolution") +RESOLUTION = { + "16_BITS": ADS1115Resolution.ADS1115_16_BITS, + "12_BITS": ADS1115Resolution.ADS1015_12_BITS, +} + +ADS1115Samplerate = ads1115_int_ns.enum("ADS1115Samplerate") +SAMPLERATE = { + "8": ADS1115Samplerate.ADS1115_8SPS, + "16": ADS1115Samplerate.ADS1115_16SPS, + "32": ADS1115Samplerate.ADS1115_32SPS, + "64": ADS1115Samplerate.ADS1115_64SPS, + "128": ADS1115Samplerate.ADS1115_128SPS, + "250": ADS1115Samplerate.ADS1115_250SPS, + "475": ADS1115Samplerate.ADS1115_475SPS, + "860": ADS1115Samplerate.ADS1115_860SPS, +} + +ADS1115Sensor = ads1115_int_ns.class_( + "ADS1115Sensor", sensor.Sensor, cg.Component #, voltage_sampler.VoltageSampler +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + ADS1115Sensor, + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(CONF_ADS1115_ID): cv.use_id(ADS1115Component), + cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space="_"), + cv.Required(CONF_GAIN): cv.enum(GAIN, string=True), + cv.Optional(CONF_RESOLUTION, default="16_BITS"): cv.enum( + RESOLUTION, upper=True, space="_" + ), + cv.Optional(CONF_SAMPLE_RATE, default="860"): cv.enum( + SAMPLERATE, string=True + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await sensor.register_sensor(var, config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_ADS1115_ID]) + + cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER])) + cg.add(var.set_gain(config[CONF_GAIN])) + cg.add(var.set_resolution(config[CONF_RESOLUTION])) + cg.add(var.set_samplerate(config[CONF_SAMPLE_RATE])) diff --git a/ads1115_int/sensor/ads1115_int_sensor.cpp b/ads1115_int/sensor/ads1115_int_sensor.cpp new file mode 100644 index 0000000..3fe69f7 --- /dev/null +++ b/ads1115_int/sensor/ads1115_int_sensor.cpp @@ -0,0 +1,28 @@ +#include "ads1115_int_sensor.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace ads1115_int { + +static const char *const TAG = "ads1115_int.sensor"; + +void ADS1115Sensor::loop() +{ + if(this->first_reading_) { + int mux_index = this->parent_->set_mux_data(this->multiplexer_, this->gain_, this->resolution_, this->samplerate_, this); + this->first_reading_ = false; + this->parent_->start_conversion(mux_index); // this will set off the first conversion; subsequent conversions will be triggered by the ISR + } +} + +void ADS1115Sensor::dump_config() { + LOG_SENSOR(" ", "ADS1115 Int Sensor", this); + ESP_LOGCONFIG(TAG, " Multiplexer: %u", this->multiplexer_); + ESP_LOGCONFIG(TAG, " Gain: %u", this->gain_); + ESP_LOGCONFIG(TAG, " Resolution: %u", this->resolution_); + ESP_LOGCONFIG(TAG, " Sample rate: %u", this->samplerate_); +} + +} // namespace ads1115 +} // namespace esphome diff --git a/ads1115_int/sensor/ads1115_int_sensor.h b/ads1115_int/sensor/ads1115_int_sensor.h new file mode 100644 index 0000000..e9722d7 --- /dev/null +++ b/ads1115_int/sensor/ads1115_int_sensor.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include "esphome/components/sensor/sensor.h" +//#include "esphome/components/voltage_sampler/voltage_sampler.h" + +#include "../ads1115_int.h" + +namespace esphome { +namespace ads1115_int { + +/// Internal holder class that is in instance of Sensor so that the hub can create individual sensors. +class ADS1115Sensor : public sensor::Sensor, + public Component, + //public voltage_sampler::VoltageSampler, + public Parented { + public: + void loop() override; + void set_multiplexer(ADS1115Multiplexer multiplexer) { this->multiplexer_ = multiplexer; } + void set_gain(ADS1115Gain gain) { this->gain_ = gain; } + void set_resolution(ADS1115Resolution resolution) { this->resolution_ = resolution; } + void set_samplerate(ADS1115Samplerate samplerate) { this->samplerate_ = samplerate; } + void dump_config() override; + + protected: + ADS1115Multiplexer multiplexer_; + ADS1115Gain gain_; + ADS1115Resolution resolution_; + ADS1115Samplerate samplerate_; + bool first_reading_{true}; + +}; + +} // namespace ads1115 +} // namespace esphome diff --git a/ads1115_pol/__init__.py b/ads1115_pol/__init__.py new file mode 100644 index 0000000..503cd54 --- /dev/null +++ b/ads1115_pol/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv +from esphome.const import CONF_ID + +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +ads1115_pol_ns = cg.esphome_ns.namespace("ads1115_pol") +ADS1115Component = ads1115_pol_ns.class_("ADS1115Component", cg.Component, i2c.I2CDevice) + +CONF_CONTINUOUS_MODE = "continuous_mode" +CONF_ADS1115_ID = "ads1115_id" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ADS1115Component), + cv.Optional(CONF_CONTINUOUS_MODE, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(None)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + cg.add(var.set_continuous_mode(config[CONF_CONTINUOUS_MODE])) diff --git a/ads1115_pol/ads1115_pol.cpp b/ads1115_pol/ads1115_pol.cpp new file mode 100644 index 0000000..f099e29 --- /dev/null +++ b/ads1115_pol/ads1115_pol.cpp @@ -0,0 +1,223 @@ +#include "ads1115_pol.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ads1115_pol { + +static const char *const TAG = "ads1115_pol"; +static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00; +static const uint8_t ADS1115_REGISTER_CONFIG = 0x01; + +void ADS1115Component::setup() { + uint16_t value; + if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)) { + this->mark_failed(/*LOG_STR*/("Could not read ADS1115 conversion register on setup.")); + return; + } + + uint16_t config = 0; + // Clear single-shot bit + // 0b0xxxxxxxxxxxxxxx + config |= 0b0000000000000000; + // Setup multiplexer + // 0bx000xxxxxxxxxxxx + config |= ADS1115_MULTIPLEXER_P0_N1 << 12; + + // Setup Gain + // 0bxxxx000xxxxxxxxx + config |= ADS1115_GAIN_6P144 << 9; + + if (this->continuous_mode_) { + // Set continuous mode + // 0bxxxxxxx0xxxxxxxx + config |= 0b0000000000000000; + } else { + // Set singleshot mode + // 0bxxxxxxx1xxxxxxxx + config |= 0b0000000100000000; + } + + // Set data rate - 860 samples per second + // 0bxxxxxxxx100xxxxx + config |= ADS1115_860SPS << 5; + + // Set comparator mode - hysteresis + // 0bxxxxxxxxxxx0xxxx + config |= 0b0000000000000000; + + // Set comparator polarity - active low + // 0bxxxxxxxxxxxx0xxx + config |= 0b0000000000000000; + + // Set comparator latch enabled - false + // 0bxxxxxxxxxxxxx0xx + config |= 0b0000000000000000; + + // Set comparator que mode - disabled + // 0bxxxxxxxxxxxxxx11 + config |= 0b0000000000000011; + + if (!this->write_byte_16(ADS1115_REGISTER_CONFIG, config)) { + this->mark_failed(/*LOG_STR*/("Could not write to ADS1115 config register on setup.")); + return; + } + this->prev_config_ = config; +} +void ADS1115Component::dump_config() { + ESP_LOGCONFIG(TAG, "ADS1115_pol:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} +float ADS1115Component::request_measurement(int16_t& raw_value, ADS1115Multiplexer multiplexer, ADS1115Gain gain, + ADS1115Resolution resolution, ADS1115Samplerate samplerate) { + uint16_t config = this->prev_config_; + // Multiplexer + // 0bxBBBxxxxxxxxxxxx + config &= 0b1000111111111111; + config |= (multiplexer & 0b111) << 12; + + // Gain + // 0bxxxxBBBxxxxxxxxx + config &= 0b1111000111111111; + config |= (gain & 0b111) << 9; + + // Sample rate + // 0bxxxxxxxxBBBxxxxx + config &= 0b1111111100011111; + config |= (samplerate & 0b111) << 5; + + if (!this->continuous_mode_) { + // Start conversion + config |= 0b1000000000000000; + } + + if (!this->continuous_mode_ || this->prev_config_ != config) { + if (!this->write_byte_16(ADS1115_REGISTER_CONFIG, config)) { + this->status_set_warning(); + return NAN; + } + this->prev_config_ = config; + + // Delay calculated as: ceil((1000/SPS)+.5) + if (resolution == ADS1015_12_BITS) { + switch (samplerate) { + case ADS1115_8SPS: + delay(9); + break; + case ADS1115_16SPS: + delay(5); + break; + case ADS1115_32SPS: + delay(3); + break; + case ADS1115_64SPS: + case ADS1115_128SPS: + delay(2); + break; + default: + delay(1); + break; + } + } else { + switch (samplerate) { + case ADS1115_8SPS: + delay(126); // NOLINT + break; + case ADS1115_16SPS: + delay(63); // NOLINT + break; + case ADS1115_32SPS: + delay(32); + break; + case ADS1115_64SPS: + delay(17); + break; + case ADS1115_128SPS: + delay(9); + break; + case ADS1115_250SPS: + delay(5); + break; + case ADS1115_475SPS: + delay(3); + break; + case ADS1115_860SPS: + delay(2); + break; + } + } + + // in continuous mode, conversion will always be running, rely on the delay + // to ensure conversion is taking place with the correct settings + // can we use the rdy pin to trigger when a conversion is done? + if (!this->continuous_mode_) { + uint32_t start = millis(); + while (this->read_byte_16(ADS1115_REGISTER_CONFIG, &config) && (config >> 15) == 0) { + if (millis() - start > 100) { + ESP_LOGW(TAG, "Reading ADS1115 timed out"); + this->status_set_warning(); + return NAN; + } + yield(); + } + } + } + + uint16_t raw_conversion; + if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &raw_conversion)) { + this->status_set_warning(); + return NAN; + } + + if (resolution == ADS1015_12_BITS) { + bool negative = (raw_conversion >> 15) == 1; + + // shift raw_conversion as it's only 12-bits, left justified + raw_conversion = raw_conversion >> (16 - ADS1015_12_BITS); + + // check if number was negative in order to keep the sign + if (negative) { + // the number was negative + // 1) set the negative bit back + raw_conversion |= 0x8000; + // 2) reset the former (shifted) negative bit + raw_conversion &= 0xF7FF; + } + } + + auto signed_conversion = static_cast(raw_conversion); + raw_value = signed_conversion; + float millivolts; + float divider = (resolution == ADS1115_16_BITS) ? 32768.0f : 2048.0f; + switch (gain) { + case ADS1115_GAIN_6P144: + millivolts = (signed_conversion * 6144) / divider; + break; + case ADS1115_GAIN_4P096: + millivolts = (signed_conversion * 4096) / divider; + break; + case ADS1115_GAIN_2P048: + millivolts = (signed_conversion * 2048) / divider; + break; + case ADS1115_GAIN_1P024: + millivolts = (signed_conversion * 1024) / divider; + break; + case ADS1115_GAIN_0P512: + millivolts = (signed_conversion * 512) / divider; + break; + case ADS1115_GAIN_0P256: + millivolts = (signed_conversion * 256) / divider; + break; + default: + millivolts = NAN; + } + + this->status_clear_warning(); + return millivolts / 1e3f; +} + +} // namespace ads1115 +} // namespace esphome diff --git a/ads1115_pol/ads1115_pol.h b/ads1115_pol/ads1115_pol.h new file mode 100644 index 0000000..006ea07 --- /dev/null +++ b/ads1115_pol/ads1115_pol.h @@ -0,0 +1,64 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" + +#include + +namespace esphome { +namespace ads1115_pol { + +enum ADS1115Multiplexer { + ADS1115_MULTIPLEXER_P0_N1 = 0b000, + ADS1115_MULTIPLEXER_P0_N3 = 0b001, + ADS1115_MULTIPLEXER_P1_N3 = 0b010, + ADS1115_MULTIPLEXER_P2_N3 = 0b011, + ADS1115_MULTIPLEXER_P0_NG = 0b100, + ADS1115_MULTIPLEXER_P1_NG = 0b101, + ADS1115_MULTIPLEXER_P2_NG = 0b110, + ADS1115_MULTIPLEXER_P3_NG = 0b111, +}; + +enum ADS1115Gain { + ADS1115_GAIN_6P144 = 0b000, + ADS1115_GAIN_4P096 = 0b001, + ADS1115_GAIN_2P048 = 0b010, + ADS1115_GAIN_1P024 = 0b011, + ADS1115_GAIN_0P512 = 0b100, + ADS1115_GAIN_0P256 = 0b101, +}; + +enum ADS1115Resolution { + ADS1115_16_BITS = 16, + ADS1015_12_BITS = 12, +}; + +enum ADS1115Samplerate { + ADS1115_8SPS = 0b000, + ADS1115_16SPS = 0b001, + ADS1115_32SPS = 0b010, + ADS1115_64SPS = 0b011, + ADS1115_128SPS = 0b100, + ADS1115_250SPS = 0b101, + ADS1115_475SPS = 0b110, + ADS1115_860SPS = 0b111 +}; + +class ADS1115Component : public Component, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + /// HARDWARE_LATE setup priority + void set_continuous_mode(bool continuous_mode) { continuous_mode_ = continuous_mode; } + + /// Helper method to request a measurement from a sensor. + float request_measurement(int16_t& raw_value, ADS1115Multiplexer multiplexer, ADS1115Gain gain, ADS1115Resolution resolution, + ADS1115Samplerate samplerate); + + protected: + uint16_t prev_config_{0}; + bool continuous_mode_; +}; + +} // namespace ads1115 +} // namespace esphome diff --git a/ads1115_pol/sensor/__init__.py b/ads1115_pol/sensor/__init__.py new file mode 100644 index 0000000..fcbeae2 --- /dev/null +++ b/ads1115_pol/sensor/__init__.py @@ -0,0 +1,98 @@ +import esphome.codegen as cg +from esphome.components import sensor, voltage_sampler +import esphome.config_validation as cv +from esphome.const import ( + CONF_GAIN, + CONF_ID, + CONF_MULTIPLEXER, + CONF_RESOLUTION, + CONF_SAMPLE_RATE, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_VOLT, +) + +from .. import CONF_ADS1115_ID, ADS1115Component, ads1115_pol_ns + +AUTO_LOAD = ["voltage_sampler"] +DEPENDENCIES = ["ads1115_pol"] + +ADS1115Multiplexer = ads1115_pol_ns.enum("ADS1115Multiplexer") +MUX = { + "A0_A1": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_N1, + "A0_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_N3, + "A1_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P1_N3, + "A2_A3": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P2_N3, + "A0_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_NG, + "A1_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P1_NG, + "A2_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P2_NG, + "A3_GND": ADS1115Multiplexer.ADS1115_MULTIPLEXER_P3_NG, +} + +ADS1115Gain = ads1115_pol_ns.enum("ADS1115Gain") +GAIN = { + "6.144": ADS1115Gain.ADS1115_GAIN_6P144, + "4.096": ADS1115Gain.ADS1115_GAIN_4P096, + "2.048": ADS1115Gain.ADS1115_GAIN_2P048, + "1.024": ADS1115Gain.ADS1115_GAIN_1P024, + "0.512": ADS1115Gain.ADS1115_GAIN_0P512, + "0.256": ADS1115Gain.ADS1115_GAIN_0P256, +} + +ADS1115Resolution = ads1115_pol_ns.enum("ADS1115Resolution") +RESOLUTION = { + "16_BITS": ADS1115Resolution.ADS1115_16_BITS, + "12_BITS": ADS1115Resolution.ADS1015_12_BITS, +} + +ADS1115Samplerate = ads1115_pol_ns.enum("ADS1115Samplerate") +SAMPLERATE = { + "8": ADS1115Samplerate.ADS1115_8SPS, + "16": ADS1115Samplerate.ADS1115_16SPS, + "32": ADS1115Samplerate.ADS1115_32SPS, + "64": ADS1115Samplerate.ADS1115_64SPS, + "128": ADS1115Samplerate.ADS1115_128SPS, + "250": ADS1115Samplerate.ADS1115_250SPS, + "475": ADS1115Samplerate.ADS1115_475SPS, + "860": ADS1115Samplerate.ADS1115_860SPS, +} + +ADS1115Sensor = ads1115_pol_ns.class_( + "ADS1115Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + ADS1115Sensor, + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(CONF_ADS1115_ID): cv.use_id(ADS1115Component), + cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space="_"), + cv.Required(CONF_GAIN): cv.enum(GAIN, string=True), + cv.Optional(CONF_RESOLUTION, default="16_BITS"): cv.enum( + RESOLUTION, upper=True, space="_" + ), + cv.Optional(CONF_SAMPLE_RATE, default="860"): cv.enum( + SAMPLERATE, string=True + ), + } + ) + .extend(cv.polling_component_schema("60s")) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await sensor.register_sensor(var, config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_ADS1115_ID]) + + cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER])) + cg.add(var.set_gain(config[CONF_GAIN])) + cg.add(var.set_resolution(config[CONF_RESOLUTION])) + cg.add(var.set_samplerate(config[CONF_SAMPLE_RATE])) diff --git a/ads1115_pol/sensor/ads1115_pol_sensor.cpp b/ads1115_pol/sensor/ads1115_pol_sensor.cpp new file mode 100644 index 0000000..a47e8cf --- /dev/null +++ b/ads1115_pol/sensor/ads1115_pol_sensor.cpp @@ -0,0 +1,36 @@ +#include "ads1115_pol_sensor.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace ads1115_pol { + +static const char *const TAG = "ads1115_pol.sensor"; + +float ADS1115Sensor::sample(int16_t& raw_value) { + return this->parent_->request_measurement(raw_value, this->multiplexer_, this->gain_, this->resolution_, this->samplerate_); +} + +void ADS1115Sensor::update() { + int16_t raw_value = -1000; + float v = this->sample(raw_value); + if (!std::isnan(v)) { + // if(this->count_ % 10 == 0) { + ESP_LOGI(TAG, "'% -18s': Raw=% 6d %fV", this->get_name().c_str(), raw_value, v); + // } + this->count_++; + ESP_LOGD(TAG, "'%s': Got Voltage=%fV", this->get_name().c_str(), v); + this->publish_state(v); + } +} + +void ADS1115Sensor::dump_config() { + LOG_SENSOR(" ", "ADS1115_pol Sensor", this); + ESP_LOGCONFIG(TAG, " Multiplexer: %u", this->multiplexer_); + ESP_LOGCONFIG(TAG, " Gain: %u", this->gain_); + ESP_LOGCONFIG(TAG, " Resolution: %u", this->resolution_); + ESP_LOGCONFIG(TAG, " Sample rate: %u", this->samplerate_); +} + +} // namespace ads1115 +} // namespace esphome diff --git a/ads1115_pol/sensor/ads1115_pol_sensor.h b/ads1115_pol/sensor/ads1115_pol_sensor.h new file mode 100644 index 0000000..97e5cd5 --- /dev/null +++ b/ads1115_pol/sensor/ads1115_pol_sensor.h @@ -0,0 +1,38 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" + +#include "../ads1115_pol.h" + +namespace esphome { +namespace ads1115_pol { + +/// Internal holder class that is in instance of Sensor so that the hub can create individual sensors. +class ADS1115Sensor : public sensor::Sensor, + public PollingComponent, + //public voltage_sampler::VoltageSampler, + public Parented { + public: + void update() override; + void set_multiplexer(ADS1115Multiplexer multiplexer) { this->multiplexer_ = multiplexer; } + void set_gain(ADS1115Gain gain) { this->gain_ = gain; } + void set_resolution(ADS1115Resolution resolution) { this->resolution_ = resolution; } + void set_samplerate(ADS1115Samplerate samplerate) { this->samplerate_ = samplerate; } + float sample(int16_t& raw_value); + + void dump_config() override; + + protected: + ADS1115Multiplexer multiplexer_; + ADS1115Gain gain_; + ADS1115Resolution resolution_; + ADS1115Samplerate samplerate_; + uint64_t count_{0}; +}; + +} // namespace ads1115 +} // namespace esphome diff --git a/ads131m08/README.md b/ads131m08/README.md new file mode 100644 index 0000000..b58a6bb --- /dev/null +++ b/ads131m08/README.md @@ -0,0 +1,80 @@ +# ESPHome components/ads131m08 + +This folder contains source code files and related resources for external ads131m08 component. + +## Example .yaml config + +--- + +```yaml +substitutions: + ESP32_S3_SPI0_SCK: GPIO12 + ESP32_S3_SPI0_MISO: GPIO13 + ESP32_S3_SPI0_MOSI: GPIO11 + ADC_SYNC_RESET_PIN: GPIO21 + ADC_CS_PIN: GPIO41 + ADC_DRDY_PIN: GPIO42 + +external_components: + - source: + type: local + path: components # Path relative to this YAML file + components: [ ads131m08 ] + +spi: + - id: spi_bus0 + clk_pin: ${ESP32_S3_SPI0_SCK} + mosi_pin: ${ESP32_S3_SPI0_MOSI} + miso_pin: ${ESP32_S3_SPI0_MISO} + interface: hardware + +ads131m08: + id: highres_adc + spi_id: spi_bus0 + cs_pin: ${ADC_CS_PIN} + drdy_pin: ${ADC_DRDY_PIN} + sync_reset_pin: ${ADC_SYNC_RESET_PIN} + reference_voltage: 1.25 + data_rate: 10MHz + clock_frequency: 8192kHz + oversampling_ratio: 512 + +sensor: + - platform: ads131m08 + ads131m08_id: highres_adc + channel: 0 + id: ads_ch0 + gain: 1 + input_select: normal + offset_calibration: 0 + gain_calibration: 0.986 + ac: + name: "Channel 0 AC" + id: ads_ch0_ac + dc: + name: "Channel 0 DC" + id: ads_ch0_dc + + - platform: ads131m08 + ads131m08_id: highres_adc + channel: 1 + id: ads_ch1 + gain: 1 + input_select: normal + offset_calibration: 0 + gain_calibration: 0.986 + ac: + name: "Channel 1 AC" + id: ads_ch1_ac + accuracy_decimals: 6 + state_class: measurement + device_class: voltage + dc: + name: "Channel 1 DC" + id: ads_ch1_dc + accuracy_decimals: 6 + state_class: measurement + device_class: voltage +``` + +For more information, visit the [ESPHome documentation](https://esphome.io/). diff --git a/ads131m08/__init__.py b/ads131m08/__init__.py new file mode 100644 index 0000000..ac8563d --- /dev/null +++ b/ads131m08/__init__.py @@ -0,0 +1,80 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import spi +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_REFERENCE_VOLTAGE + +MULTI_CONF = True + +CONF_DRDY_PIN = "drdy_pin" +CONF_SYNC_RESET_PIN = "sync_reset_pin" +CONF_CLOCK_FREQUENCY = "clock_frequency" +CONF_ADS131M08_ID = "ads131m08_id" +CONF_OVERSAMPLING_RATIO = "oversampling_ratio" + +DEPENDENCIES = ["spi"] + +ads131m08_ns = cg.esphome_ns.namespace("ads131m08") +ADS131M08Hub = ads131m08_ns.class_("ADS131M08Hub", cg.Component, spi.SPIDevice) + +OSR = ads131m08_ns.enum("ADS131M08_OVERSAMPLING_RATIO") +ALLOWED_OSRS = { + 128: OSR.OSR_128, + 256: OSR.OSR_256, + 512: OSR.OSR_512, + 1024: OSR.OSR_1024, + 2048: OSR.OSR_2048, + 4096: OSR.OSR_4096, + 8192: OSR.OSR_8192, + 16256: OSR.OSR_16256, +} + +ALLOWED_CLOCK_FREQUENCIES = { + 2048000: 2048000, + 4096000: 4096000, + 8192000: 8192000, + "2048kHz": 2048000, + "4096kHz": 4096000, + "8192kHz": 8192000, + "2.048MHz": 2048000, + "4.096MHz": 4096000, + "8.192MHz": 8192000, +} + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ADS131M08Hub), + cv.Required(CONF_DRDY_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_SYNC_RESET_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_CLOCK_FREQUENCY, default="8192kHz"): cv.All( + cv.frequency, cv.one_of(*ALLOWED_CLOCK_FREQUENCIES.keys()) + ), + cv.Optional(CONF_OVERSAMPLING_RATIO, default=1024): cv.enum( + ALLOWED_OSRS, int=True + ), + cv.Optional(CONF_REFERENCE_VOLTAGE, default=1.2): cv.float_range( + min=1.1, max=1.3 + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(spi.spi_device_schema(cs_pin_required=True, default_data_rate="10MHz")) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) + drdy = await cg.gpio_pin_expression(config[CONF_DRDY_PIN]) + if CONF_SYNC_RESET_PIN in config: + sync_reset = await cg.gpio_pin_expression(config[CONF_SYNC_RESET_PIN]) + cg.add(var.set_sync_reset_pin(sync_reset)) + clock_frequency = ALLOWED_CLOCK_FREQUENCIES[config[CONF_CLOCK_FREQUENCY]] + cg.add(var.set_clock_frequency(clock_frequency)) + osr = config[CONF_OVERSAMPLING_RATIO] + cg.add(var.set_osr(osr)) + reference_voltage = config[CONF_REFERENCE_VOLTAGE] + cg.add(var.set_reference_voltage(reference_voltage)) + cg.add(var.set_drdy_pin(drdy)) diff --git a/ads131m08/ads131m08.cpp b/ads131m08/ads131m08.cpp new file mode 100644 index 0000000..93c6941 --- /dev/null +++ b/ads131m08/ads131m08.cpp @@ -0,0 +1,1720 @@ +#include "ads131m08.h" +// #include +// #include +// #include "esphome/components/spi/spi.h" +#include "esphome/core/log.h" +#include +#include +#include +#include "esp_system.h" +#include "spi_flash_mmap.h" + +// using namespace esphome::spi; + +namespace esphome { +namespace ads131m08 { + +static const char *const TAG = "ads131m08"; + +// ADS131M08Hub::ADS131M08Hub() +// : base_frame(40, 0) +//{ +// } + +void ADS131M08Hub::setup() { + for (int i = 0; i < ADC_CHANNELS; i++) { + // last_publish_time_[i] = 0; + num_samples_[i] = 0; + sample_sum_[i] = 0; + sample_squared_sum_[i] = 0; + } + // 1. Create a binary semaphore + this->data_ready_semhandle = xSemaphoreCreateBinary(); + this->update_avg = xSemaphoreCreateMutex(); + sync_reset_pin_->setup(); + sync_reset_pin_->pin_mode(esphome::gpio::FLAG_OUTPUT); // Set SYNC/RESET pin as output + sync_reset_pin_->digital_write(true); + // setup DRDY pin interrupt + this->drdy_pin_->setup(); + this->drdy_pin_->pin_mode(esphome::gpio::FLAG_INPUT | esphome::gpio::FLAG_PULLUP); // external pull-up + this->spi_setup(); + // Check if setup failed + if (this->is_failed()) { + ESP_LOGE(TAG, "SPI setup failed!"); + this->mark_failed(LOG_STR("SPI setup failed.")); + return; + } + // Send RESET command + bool reset_ok = adc_reset_retry(); + // ESP_LOGW(TAG, "adc word length: %d", adc_word_length_); + if (!reset_ok) { + ESP_LOGE(TAG, "ADC reset failed!"); + this->mark_failed(LOG_STR("Reset failed.")); + this->adc_init_ = 0; + return; + } + if (!this->adc_initialize(DEFAULT_WORD_LENGTH)) { + ESP_LOGE(TAG, "ADC reset failed!"); + this->mark_failed(LOG_STR("Initialisation failed.")); + this->adc_init_ = 0; + return; + } + set_reg_osr(); + if (!adc_lock(true)) { + ESP_LOGE(TAG, "ADC lock failed!"); + this->mark_failed(LOG_STR("ADC lock failed.")); + this->adc_init_ = 0; + return; + } + if (!adc_lock(false)) { + ESP_LOGE(TAG, "ADC unlock failed!"); + this->mark_failed(LOG_STR("ADC unlock failed.")); + this->adc_init_ = 0; + return; + } + this->drdy_pin_->attach_interrupt(&ADS131M08Hub::isr_handler, this, gpio::INTERRUPT_FALLING_EDGE); + // cs_ctr_=1000; + // SPIDevice::enable(); // leave CS low + // Request a high loop() execution interval + // this->high_freq_.start(); +} + +void ADS131M08Hub::dump_config() { + ESP_LOGCONFIG(TAG, "ADS131M08:"); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DRDY Pin: ", this->drdy_pin_); + if (this->sync_reset_pin_ != nullptr) { + LOG_PIN(" SYNC/RESET Pin: ", this->sync_reset_pin_); + } + if (this->data_rate_ < 1000000) { + ESP_LOGCONFIG(TAG, " Data rate: %" PRId32 "kHz", this->data_rate_ / 1000); + } else { + ESP_LOGCONFIG(TAG, " Data rate: %" PRId32 "MHz", this->data_rate_ / 1000000); + } + if (this->clock_frequency_ < 1000000) { + ESP_LOGCONFIG(TAG, " Clock frequency: %.3fkHz", this->clock_frequency_ / 1000); + } else { + ESP_LOGCONFIG(TAG, " Clock frequency: %.3fMHz", this->clock_frequency_ / 1000000); + } + uint16_t osr = 1 << (7 + this->osr_); + if (osr == 16384) { + osr = 16256; + } + ESP_LOGCONFIG(TAG, " Oversampling ratio: %" PRId32, osr); + ESP_LOGCONFIG(TAG, " Reference Voltage: %.2fV", this->reference_voltage_); + ESP_LOGCONFIG(TAG, " SPI Mode: %d", this->mode_); + ESP_LOGCONFIG(TAG, " Bit Order: %s", + this->bit_order_ == 1 ? "msb_first" + : this->bit_order_ == 0 ? "lsb_first" + : "unknown"); +} + +uint8_t ADS131M08Hub::update_adc_word_length() { + int word_nbytes = this->adc_word_length_ >> 3; + spiframe frame(2 * word_nbytes, 0); // prepare with CMD_NULL; always cater for crc + write_array(frame); // write CMD_NULL to ADC to retrieve status + read_array(frame); + uint16_t status = frame[0] << 8 | frame[1]; + // print_command_response_to_string(CMD_NULL, frame).c_str(); + return update_adc_word_length(status); +} + +uint8_t ADS131M08Hub::update_adc_word_length(uint16_t status) { + switch (status & MASK_STATUS_WLENGTH) { + case WLENGTH_16_BITS: + this->adc_word_length_ = 16; + break; + case WLENGTH_24_BITS: + this->adc_word_length_ = 24; + break; + case WLENGTH_32_BITS_LSB_ZERO_PADDING: + case WLENGTH_32_BITS_MSB_SIGN_EXTEND: + this->adc_word_length_ = 32; + break; + default: + ESP_LOGW(TAG, "Unknown word length: %u", adc_word_length_); + break; + } + update_conversion_factor(); + return this->adc_word_length_; +} + +float ADS131M08Hub::update_conversion_factor() { + switch (adc_word_length_) { + case 16: + this->conversion_factor_ = this->reference_voltage_ / 32768.0; // TODO: must double check this with a test + break; + default: + this->conversion_factor_ = this->reference_voltage_ / 8388608.0; + break; + } + return this->conversion_factor_; +} +bool ADS131M08Hub::adc_initialize(uint8_t word_length) { + this->adc_init_++; + update_adc_word_length(); + if (!adc_register_write(REG_CLOCK, reg_clock_allch_off)) { + ESP_LOGE(TAG, "CLOCK register write / read to turn off all channels failed"); + return false; + } + ESP_LOGV(TAG, "Turned off all channels so short frames can be written during config"); + if (!adc_register_write(REG_MODE, MODE_MASK_RESET_HAPPENED & reg_mode_cfg24)) { + ESP_LOGE(TAG, "MODE register write / read to set word size to 24bits failed"); + return false; + } + // update_adc_word_length(); + ESP_LOGV(TAG, "Written MODE register; Cleared the RESET flag, made DRDY active low pulse"); + if (!adc_register_write(REG_THRSHLD_LSB, 0x09)) { + ESP_LOGE(TAG, "THRSHLD_LSB register write / read to set DBLOCK filter failed"); + return false; + } + ESP_LOGV(TAG, "Written THRSHLD_LSB register; Set DCBLOCK filter to have a corner frequency of 622 mHz"); + if (!adc_set_word_length(word_length)) + return false; + // we leave should channels off, as the individual sensors should turn it on + if (!adc_register_write(REG_CLOCK, reg_clock_allch_off)) { + ESP_LOGE(TAG, "CLOCK register write / read to turn-off all channels failed"); + return false; + } + + // ESP_LOGV(TAG, "Turned on all ADC channels; Re-wrote defaults for other bits in CLOCK register"); + if (!drdy_pin_->digital_read()) { + ESP_LOGE(TAG, "DRDY pin is low after initialization!"); + return false; + } + this->adc_init_--; + return true; +} + +bool ADS131M08Hub::adc_set_word_length(uint8_t word_length) { + uint16_t setting; + switch (word_length) { + case 32: + setting = reg_mode_cfg32; + break; + case 16: + setting = reg_mode_cfg16; + break; + default: + setting = reg_mode_cfg24; + break; + } + if (!adc_register_write_masked(REG_MODE, setting, reg_mode_cfg_mask, __LINE__)) { + ESP_LOGE(TAG, "MODE register write / read to set word size to %ubits failed", word_length); + return false; + } + ESP_LOGV(TAG, "Written MODE register; Made ADC word size %u bits. Re-wrote other set bits in MODE register", + word_length); + // update_adc_word_length(); + return true; +} + +// Implementing nested CS enable/disable handling in ADS131M08Hub to allow multiple transfers in a row without toggling +// CS between them, which is required for proper communication with the ADC according to the datasheet recommendations. +void ADS131M08Hub::enable(const char *txt) { + cs_ctr_++; // Increment counter on each enable call + // esph_log_i(TAG, "cs_ctr_e: %s %d", txt, cs_ctr_.load()); + if (cs_ctr_ == 1) { // Only set CS low on the first enable call + SPIDevice::enable(); // Call the base class enable to set CS low + if (txt != nullptr) + esph_log_i(TAG, "enable: %s", txt); + } +} + +void ADS131M08Hub::disable(const char *txt) { + // esph_log_i(TAG, "cs_ctr_d: %s %d", txt, cs_ctr.load()); + cs_ctr_--; // Decrement counter on each disable call + if (cs_ctr_ == 0) { // Only set CS high when counter returns to 0 + SPIDevice::disable(); // Call the base class disable to set CS high + if (txt != nullptr) + esph_log_i(TAG, "disable: %s", txt); + } else if (cs_ctr_ < 0) { // Sanity check to prevent counter from going negative + cs_ctr_ = 0; + } +} + +// ISR function to handle interrupt from drdy pin +// MUST be fast and non-blocking +void ADS131M08Hub::isr_handler(ADS131M08Hub *arg) { + { + InterruptLock lock; + if (arg != nullptr) { + arg->isr_ctr_++; + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + xSemaphoreGiveFromISR(arg->data_ready_semhandle, &xHigherPriorityTaskWoken); + if (xHigherPriorityTaskWoken == pdTRUE) { + portYIELD_FROM_ISR(); // Switch to the waiting task immediately + } + } + } +} + +void ADS131M08Hub::loop() { + // Check the semaphore (0 timeout means non-blocking) + if (xSemaphoreTake(data_ready_semhandle, 0) == pdTRUE) { + if (this->adc_init_ == 0) { + uint32_t num_samples = 0; + uint32_t crc_errors = 0; + { + CHIP_SELECTx + // Perform the read here safely outside of ISR + uint64_t start = micros(); + // this->txf_init(); // implementing datasheet recommended TXF init in ISR + this->adc_sync(); + if (rms_calc_req_) { + std::pair result = read_multi(); + num_samples = result.first; + crc_errors = result.second; + uint64_t end = micros(); + update_averages(SAMPLE_TIME_SENSOR, 1, static_cast(end - start) / 1000.0); + update_averages(MAX_SAMPLES_SENSOR, 1, static_cast(num_samples)); + update_averages(CRC_ERRORS_SENSOR, 1, static_cast(crc_errors)); + uint16_t ctr = isr_ctr_; + update_averages(ISR_COUNT_SENSOR, 1, static_cast(ctr)); + + // if(this->sensors_ac[SAMPLE_TIME_SENSOR] != nullptr) + // this->sensors_ac[SAMPLE_TIME_SENSOR]->publish_state(static_cast(end - start)/1000.0); + // if(this->sensors_ac[MAX_SAMPLES_SENSOR] != nullptr) + // this->sensors_ac[MAX_SAMPLES_SENSOR]->publish_state(static_cast(num_samples)); + // if(this->sensors_ac[CRC_ERRORS_SENSOR] != nullptr) + // this->sensors_ac[CRC_ERRORS_SENSOR]->publish_state(static_cast(crc_errors)); + // if(this->sensors_ac[ISR_COUNT_SENSOR] != nullptr) { + // uint16_t ctr = isr_ctr_; + // this->sensors_ac[ISR_COUNT_SENSOR]->publish_state(static_cast(ctr)); + // } + } else { + read_single(); + } + isr_ctr_ = 0; + } + if (crc_errors > 30) { + ESP_LOGW(TAG, "High CRC error rate."); + adc_set_word_length(DEFAULT_WORD_LENGTH); // for some reason the adc occasionally reverts to 24bits; this will + // reset the word length to what it should be + // update_adc_word_length(); + // spiframe recoverframe(40, 0); + // read_array(recoverframe); + // ESP_LOGI(TAG, "WL: %u Frame: %s", this->adc_word_length_, frame_to_string(recoverframe).c_str()); + } + // ESP_LOGW(TAG, "%llu ms (%llu us), max samples: %u", (end - start)/1000, (end - start), num_samples); + } + } +} +/// @brief Set 16 bit frameword +/// @param frame +/// @param w_index +/// @param data uint16_t denotes command or CRC +/// @return +bool ADS131M08Hub::set_frame_word(spiframe &frame, int w_index, uint16_t data) { + size_t word_nbytes = adc_word_length_ >> 3; + size_t frame_words = frame.size() / word_nbytes; + int b_index = w_index * word_nbytes; + if (w_index >= frame_words) { + if (w_index > MAX_FRAME_SIZE) { + ESP_LOGE(TAG, "Word index of %d out of bounds", w_index); + // esp_crosscore_int_send_print_backtrace(xPortGetCoreID()); + return false; // invalid - something has gone wrong + } + frame.resize(b_index + word_nbytes); + } + switch (adc_word_length_) { + // deliberate no breaks after first two cases + case 32: + frame[b_index + 3] = 0; + case 24: + frame[b_index + 2] = 0; + case 16: + frame[b_index + 1] = data & 0xFF; + frame[b_index] = (data >> 8) & 0xFF; + break; + default: + ESP_LOGW(TAG, "Invalid adc word length of %d", adc_word_length_); + break; + } + return true; +} + +bool ADS131M08Hub::set_frame_word(spiframe &frame, int w_index, uint32_t data) { + size_t word_nbytes = adc_word_length_ >> 3; + size_t frame_words = frame.size() / word_nbytes; + int b_index = w_index * word_nbytes; + if (w_index >= frame_words) { + if (w_index > MAX_FRAME_SIZE) { + ESP_LOGE(TAG, "Word index of %d out of bounds", w_index); + // esp_crosscore_int_send_print_backtrace(xPortGetCoreID()); + return false; // invalid - something has gone wrong + } + frame.resize(b_index + word_nbytes); + } + switch (adc_word_length_) { + // deliberate no breaks after first two cases + case 32: + frame[b_index++] = (data >> 24) & 0xFF; + case 24: + frame[b_index++] = (data >> 16) & 0xFF; + case 16: + frame[b_index++] = (data >> 8) & 0xFF; + frame[b_index] = data & 0xFF; + break; + default: + ESP_LOGW(TAG, "Invalid adc word length of %d", adc_word_length_); + break; + } + return true; +} + +// index(i) = zero-reference, returns i'th word, LSB aligned +uint32_t ADS131M08Hub::get_unsigned_frame_word(const spiframe &frame, int w_index, bool force_16bits) { + uint32_t result = 0; + size_t word_nbytes = adc_word_length_ >> 3; + int b_index = w_index * word_nbytes; + if (b_index > (frame.size() - word_nbytes)) { + return result; // illegal + } + switch (force_16bits ? 16 : adc_word_length_) { + // deliberate no breaks after first two cases + case 32: + result += frame[b_index++] << 24; + case 24: + result += frame[b_index++] << 16; + case 16: + result += frame[b_index++] << 8; + result += frame[b_index]; + break; + default: + ESP_LOGW(TAG, "Invalid adc word length of %d", adc_word_length_); + break; + } + return result; +} + +int32_t ADS131M08Hub::get_sign_ext_frame_word(const spiframe &frame, int w_index) { + int32_t result = 0; + size_t word_nbytes = adc_word_length_ >> 3; + int b_index = w_index * word_nbytes; + if (b_index > (frame.size() - word_nbytes)) { + ESP_LOGW(TAG, "Word index of %d out of bounds", w_index); + return result; // invalid + } + uint32_t raw = 0; + switch (adc_word_length_) { + case 32: + raw = (frame[b_index] << 24) | (frame[b_index + 1] << 16) | (frame[b_index + 2]) << 8 | frame[b_index + 3]; + result = static_cast(raw); + break; + case 24: + raw = (frame[b_index] << 16) | (frame[b_index + 1] << 8) | frame[b_index + 2]; + if (raw & 0x800000) + result = static_cast(raw | 0xFF000000); // Sign extend + else + result = static_cast(raw); + break; + case 16: + raw = ((frame[b_index] << 8) | frame[b_index + 1]); + result = static_cast(raw); + break; + default: + ESP_LOGW(TAG, "Invalid adc word length of %d", adc_word_length_); + break; + } + return result; +} + +void ADS131M08Hub::adc_hard_reset() { + float cycle_micros = 1000000 / this->clock_frequency_; // one cycle duration in micro seconds + uint32_t reset_pulse_duration = static_cast( + 1.05 * 2048 * cycle_micros); // 2048 clock cycles converted to microseconds, multiplied by 1.05 to ensure we meet + // the minimum pulse duration requirement + if (reset_pulse_duration < 1) { + reset_pulse_duration = 1; // set it to 1 microsecond + } + sync_reset_pin_->digital_write(false); // Pull SYNC/RESET pin low to reset the ADC + delay_microseconds_safe(reset_pulse_duration); // Hold reset for the calculated duration + sync_reset_pin_->digital_write(true); // Set SYNC/RESET pin high to allow normal operation +} +void ADS131M08Hub::adc_sync() { + float cycle_micros = 1000000 / this->clock_frequency_; // one cycle duration in micro seconds + uint32_t sync_pulse_duration = + static_cast(std::round(50 * cycle_micros)); // minimum is 1/clock freq, max = 2047/clock freq + if (sync_pulse_duration < 1) { + sync_pulse_duration = 1; // set it to 1 microsecond + } + sync_reset_pin_->digital_write(false); // Pull SYNC/RESET pin low to sync the ADC + delay_microseconds_safe(sync_pulse_duration); // Hold sync for the calculated duration + sync_reset_pin_->digital_write(true); // Set SYNC/RESET pin high to allow normal operation +} + +// ********************** from datasheet ************************ +void ADS131M08Hub::txf_init() { + CHIP_SELECT + if (firstRead) { + // Clear the ADC's 2-deep FIFO on the first read + for (int i = 0; i < numFrameWords * adc_word_length_; i++) { + this->write_byte(0); + } + for (int i = 0; i < numFrameWords; i++) { + this->read_byte(); // should perhaps read dwords instead of bytes, but we just want to clear the FIFO so it + // doesn't matter much? + } + firstRead = false; // Clear the flag + // we will do this later, after sorting out basic communication + // DMA.enable(); // Let the DMA start sending ADC data to memory + } + // Send the dummy data to the ADC to get the ADC data + for (int i = 0; i < numFrameWords * adc_word_length_; i++) { + this->write_byte(0); + } +} + +bool ADS131M08Hub::adc_reset_retry() { + adc_word_length_ = 24; + bool reset_ok = adc_soft_reset(); + uint8_t retry_wordlengths[] = {32, 32, 24, 24, 16, 16}; + int i = 0; + while (i < sizeof(retry_wordlengths) && !reset_ok) { + adc_word_length_ = retry_wordlengths[i]; + // ESP_LOGW(TAG, "ADC word length: %d", adc_word_length_); + reset_ok = adc_soft_reset(); + i++; + } + if (!reset_ok) { + adc_hard_reset(); + delay_microseconds_safe(10000); + i = 0; + while (i < sizeof(retry_wordlengths) && !reset_ok) { + adc_word_length_ = retry_wordlengths[i]; + // ESP_LOGW(TAG, "ADC word length: %d", adc_word_length_); + reset_ok = adc_soft_reset(); + i++; + } + } + return reset_ok; +} + +// int ADS131M08Hub::build_frame() +bool ADS131M08Hub::adc_soft_reset() { + int index = 0; + uint16_t reset_response; + uint16_t cmd = 0; + update_adc_word_length(); + size_t word_nbytes = adc_word_length_ >> 3; + int framelength = numFrameWords * word_nbytes; + spiframe frame(framelength, 0); + size_t frame_len; + // curly braces confine chip select scope + { + CHIP_SELECTx cmd = CMD_RESET; + set_frame_word(frame, 0, cmd); + frame.resize(2 * word_nbytes); + frame_len = add_crc(frame); + frame.resize(framelength); + index += frame_len; + for (; index < framelength; index++) { + frame[index] = 0; + } + write_array(frame); + ESP_LOGVV(TAG, "Sent Frame: %s", frame_to_string(frame).c_str()); + } + delay_microseconds_safe(T_REGACQ); + update_adc_word_length(WLENGTH_24_BITS); // should be 24 after reset + word_nbytes = adc_word_length_ >> 3; + frame.resize(numFrameWords * word_nbytes); + { + CHIP_SELECTx read_array(frame); + reset_response = get_unsigned_frame_word(frame, 0, true); + bool crc_ok = check_crc(frame); + ESP_LOGVV(TAG, "Read Frame: %s CRC: %s", frame_to_string(frame).c_str(), (crc_ok ? "OK" : "FAIL")); + } + bool result = reset_response == RSP_RESET_OK; + ESP_LOGVV(TAG, "Reset response: [0x%04X] %s", reset_response, (result ? "OK" : "FAIL")); + return result; +} + +// overwrites last (or second) frameword with CRC, therefore frame should be large enough for payload and crc +size_t ADS131M08Hub::add_crc(spiframe &frame) { + size_t frame_length = frame.size(); + if (frame_length == 0 || adc_word_length_ == 0) + return 0; + size_t word_nbytes = adc_word_length_ >> 3; + size_t frame_words = frame_length / word_nbytes; // integer division, we discard remainder if any + size_t payload_len = word_nbytes * (frame_words - 1); + if (payload_len < word_nbytes) + return false; + uint16_t crc = get_crc(frame); + set_frame_word(frame, frame_words - 1, crc); + return frame_words * word_nbytes; +} + +// CRC should be 16bits, MSB aligned in last word of frame +bool ADS131M08Hub::check_crc(const spiframe &frame) { + if (adc_word_length_ == 0) + return false; + size_t word_nbytes = adc_word_length_ >> 3; + int frame_length = frame.size(); + size_t frame_words = frame_length / word_nbytes; // integer division, we discard remainder if any + size_t payload_len = word_nbytes * (frame_words - 1); + if (payload_len < word_nbytes) + return false; + uint16_t frame_crc = (frame[payload_len] << 8) + frame[payload_len + 1]; // Frames are MSB aligned, so is CRC + auto crc = get_crc(frame); + bool crc_ok = crc == frame_crc; + return crc_ok; +} + +uint16_t ADS131M08Hub::read_frame_crc(const spiframe &frame) { + if (adc_word_length_ == 0) + return 0; + size_t word_nbytes = adc_word_length_ >> 3; + int frame_length = frame.size(); + size_t frame_words = frame_length / word_nbytes; // integer division, we discard remainder if any + size_t payload_len = word_nbytes * (frame_words - 1); + if (payload_len < word_nbytes) + return 0; + return (frame[payload_len] << 8) + frame[payload_len + 1]; // Frames are MSB aligned, so is CRC +} +/* +CRC Parameters for ADS131M08 +Polynomial: 0x1021 (x^16 + x^12 + x^5 + 1). +Initial Value (Seed): 0xFFFF. +Input Data: The calculation is performed over all bits in the SPI frame preceding the CRC word. +Output Format: The resulting 16-bit CRC is placed in the most significant 16 bits of the final 24-bit (or 32-bit) SPI +word; the remaining bits are typically padded with zeros. +*/ +uint16_t ADS131M08Hub::get_crc(const spiframe &frame) { + size_t word_nbytes = adc_word_length_ >> 3; + size_t frame_words = frame.size() / word_nbytes; // integer division, we discard remainder if any + size_t payload_len = word_nbytes * (frame_words - 1); + uint16_t crc_result = crc(0xFFFF, frame[0]); + for (int j = 1; j < payload_len; j++) { + crc_result = crc(crc_result, frame[j]); + } + return crc_result; +} + +uint16_t ADS131M08Hub::crc(uint16_t crc_register, uint8_t data) { + uint16_t xor_result, input_bit, crc_result; + crc_result = crc_register; + for (int i = 7; i >= 0; i--) { + input_bit = (data >> i) & 0x01; + xor_result = input_bit ^ (crc_result & 0x8000) >> 15; + crc_result = crc_result << 1; + if (xor_result) + crc_result = crc_result ^ 0x1021; + } + return crc_result; +} + +void ADS131M08Hub::write_byte(uint8_t byte) { + CHIP_SELECT + this->transfer_byte(byte); +} + +uint8_t ADS131M08Hub::read_byte() { + CHIP_SELECT + uint8_t result = this->transfer_byte(0x00); // Send dummy byte to read data + return result; +} + +void ADS131M08Hub::read_array(spiframe &frame) { + CHIP_SELECT + for (size_t i = 0; i < frame.size(); i++) { + frame[i] = this->transfer_byte(0x00); // Send dummy byte to read data + } +} + +void ADS131M08Hub::write_array(const spiframe &frame) { + CHIP_SELECT + for (size_t i = 0; i < frame.size(); i++) { + this->transfer_byte(frame[i]); + } +} + +void ADS131M08Hub::transfer_array(const spiframe &tx_frame, spiframe &rx_frame) { + CHIP_SELECT + auto frame_length = tx_frame.size(); + if (rx_frame.size() < frame_length) + rx_frame.resize(frame_length); + for (size_t i = 0; i < frame_length; i++) { + rx_frame[i] = this->transfer_byte(tx_frame[i]); + } +} + +bool ADS131M08Hub::set_measure_rms(uint8_t channel, bool enable) { + if (channel >= MAX_CHANNELS) + return false; + this->rms_enabled_[channel] = enable; + this->rms_calc_req_ = true; + return true; +} + +void ADS131M08Hub::read_single() { + int word_nbytes = adc_word_length_ >> 3; + int frame_length = numFrameWords * word_nbytes; + spiframe frame(frame_length, 0); + this->read_array(frame); + bool crc_ok = check_crc(frame); + uint16_t drdy_status = get_unsigned_frame_word(frame, 0, true) & MASK_STATUS_DRDY; + // data_ready = crc_ok && (drdy_status != 0); + if (crc_ok) { + // sample channels + for (int i = 0; i < ADC_CHANNELS; i++) { + bool channel_ready = drdy_status & 1; + drdy_status = drdy_status >> 1; + bool ac_sensor_exist = this->sensors_ac[i] != nullptr; + bool dc_sensor_exist = this->sensors_dc[i] != nullptr; + if (channel_ready && (ac_sensor_exist || dc_sensor_exist)) { + int32_t raw = get_sign_ext_frame_word(frame, i + 1); + float value = this->conversion_factor_ * raw; + this->sampled_values_[i] = value; + if (ac_sensor_exist) + this->sensors_ac[i]->publish_state(value); + if (dc_sensor_exist) + this->sensors_dc[i]->publish_state(value); + } + } + } +} + +// do not like what the following procedure does at all (it blocks for +-40ms), but at the moment do not know how to get +// DRDY interrupt / ISR / loop setup to respond fast enough to read multiple frames with no or the minimum possible +// missed frames in quick succession idealy would want to read +- 200 24/32bit word frames in 2 AC 50/60Hz cycles to +// hopefully reliably calculate RMS value, phase offsets, etc. the best the read_multi procedure does is to read about +// 66x 16bit / 33x 32bit word frames in 40ms which is far from ideal Once we ge the ISR and loop setup to respond fast +// enough, the following procedure should be completely re-designed +std::pair ADS131M08Hub::read_multi() { + int32_t raw; + float value; + std::pair result; + result.first = 0; + result.second = 0; + bool crc_ok, data_ready, ac_sensor_exist, dc_sensor_exist; + uint16_t drdy_status, status; + int i, max_iters = 250; // going above 255 will result in overflows + uint32_t elapsed_time = 0; + uint32_t loop_start_time = micros(); + int word_nbytes = adc_word_length_ >> 3; + int frame_length = numFrameWords * word_nbytes; + spiframe frame(frame_length, 0); + // integrate for rms values over #iterations or sample_time depending on what comes first + for (int i = 0; i < ADC_CHANNELS; i++) { + // last_publish_time_[i] = 0; + num_samples_[i] = 0; + sample_sum_[i] = 0; + sample_squared_sum_[i] = 0; + } + while (elapsed_time < this->sample_time_ && max_iters-- > 0) { + this->read_array(frame); + // ESP_LOGD(TAG, "max_iters: %d, frame: %s", max_iters, frame_to_string(frame).c_str()); + status = get_unsigned_frame_word(frame, 0, true); + update_adc_word_length(status); + crc_ok = check_crc(frame); + if (crc_ok) { + // skip frame immediately afte resync + if ((status & MASK_STATUS_RESYNC) == 0) { + drdy_status = status & MASK_STATUS_DRDY; + // sample channels + for (i = 0; i < ADC_CHANNELS && drdy_status != 0; i++) { + data_ready = drdy_status & 1; + drdy_status = drdy_status >> 1; + ac_sensor_exist = this->sensors_ac[i] != nullptr; + dc_sensor_exist = this->sensors_dc[i] != nullptr; + if (data_ready && (ac_sensor_exist || dc_sensor_exist)) { + raw = get_sign_ext_frame_word(frame, i + 1); + value = this->conversion_factor_ * raw; + this->sampled_values_[i] = value; + if (this->rms_enabled_[i]) { + (this->num_samples_[i])++; + (this->sample_sum_[i]) += static_cast(raw); + (this->sample_squared_sum_[i]) += value * value; + } else { + // if(ac_sensor_exist) + // this->sensors_ac[i]->publish_state(value); + // if(dc_sensor_exist) + // this->sensors_dc[i]->publish_state(value); + update_averages(i, value, value); + } + } + } + } + } else { + result.second++; + // ESP_LOGW(TAG, "max_iters: %d, frame: %s CRC error", max_iters, frame_to_string(frame).c_str()); + } + elapsed_time = micros() - loop_start_time; + } + // publish integrated values + for (i = 0; i < ADC_CHANNELS; i++) { + if (this->rms_enabled_[i]) { + ac_sensor_exist = this->sensors_ac[i] != nullptr; + dc_sensor_exist = this->sensors_dc[i] != nullptr; + auto num_samples = this->num_samples_[i]; + if (num_samples > result.first) { + result.first = num_samples; + } + if (num_samples == 0) { + // should never happen, but let's play safe and avoid dividing by zero + ESP_LOGW(TAG, "Num samples: %d", num_samples); + update_averages(i, NAN, NAN); + // if(ac_sensor_exist) + // this->sensors_ac[i]->publish_state(NAN); + // if(dc_sensor_exist) + // this->sensors_dc[i]->publish_state(NAN); + } else { + int64_t sample_sum = sample_sum_[i]; + float rms_dc = this->conversion_factor_ * static_cast(sample_sum) / num_samples; + float rms_ac_squared = sample_squared_sum_[i] / num_samples - rms_dc * rms_dc; + float rms_ac = 0; + if (rms_ac_squared > 0) { + rms_ac = std::sqrtf(rms_ac_squared); + } + this->sps_ = (1e6 * (float) num_samples) / elapsed_time; + update_averages(i, rms_ac, rms_dc); + } + this->num_samples_[i] = 0; + this->sample_sum_[i] = 0.0f; + this->sample_squared_sum_[i] = 0.0f; + } + } + return result; +} +/// @brief Updates rolling averages for rms_ac and rms_dc. Window = 2 +/// @param channel +/// @param rms_ac +/// @param rms_dc +/// @return +bool ADS131M08Hub::update_averages(uint8_t channel, float rms_ac, float rms_dc) { + if (update_avg == nullptr || channel >= MAX_CHANNELS) { + ESP_LOGW(TAG, "channel %d not ready", channel); + return false; + } + bool success = false; + bool ac_sensor_exist = this->sensors_ac[channel] != nullptr; + bool dc_sensor_exist = this->sensors_dc[channel] != nullptr; + if (!ac_sensor_exist && !dc_sensor_exist) { + ESP_LOGW(TAG, "channel %d does not exist", channel); + return false; + } + // we treat update state as a critical resource that must not be clobbered + if (xSemaphoreTake(update_avg, (TickType_t) 50) == pdTRUE) { + uint8_t update_state = update_state_[channel]; + if (ac_sensor_exist) { + // if this is the first value, then we just store it with no averaging + if ((update_state & 0x0F) == 0) { + avg_ac_[channel] = rms_ac; + update_state |= 0x01; + } else { + // if we have NAN (no samples), we store it, but also also treat next valid value as the first + if (rms_ac == NAN) { + avg_ac_[channel] = NAN; + update_state &= 0xF0; + } else { + ESP_LOGI(TAG, "Setting AC avg: %f %f", avg_ac_[channel], rms_ac); + avg_ac_[channel] = 0.5 * (avg_ac_[channel] + rms_ac); + } + } + update_state_[channel] = update_state; + } else { + update_state_[channel] &= 0xF0; + } + update_state = update_state_[channel]; + if (dc_sensor_exist) { + if ((update_state & 0xF0) == 0) { + avg_dc_[channel] = rms_dc; + update_state |= 0x10; + } else { + if (rms_dc == NAN) { + avg_dc_[channel] = NAN; + update_state &= 0x0F; + } else { + ESP_LOGI(TAG, "Setting DC avg: %f %f", avg_dc_[channel], rms_dc); + avg_dc_[channel] = 0.5 * (avg_dc_[channel] + rms_dc); + } + } + update_state_[channel] = update_state; + } else { + update_state_[channel] &= 0x0F; + } + success = true; + xSemaphoreGive(update_avg); + } + return success; +} + +float ADS131M08Hub::get_average(uint8_t channel, bool read_ac) { + float result = NAN; + if (update_avg == nullptr || channel >= MAX_CHANNELS) { + return result; + } + if (update_avg != nullptr) { + if (xSemaphoreTake(update_avg, (TickType_t) 50) == pdTRUE) { + if (read_ac) { + // ESP_LOGI(TAG, "AC: update_state_[%u]: %02X", channel, update_state_[channel]); + if (update_state_[channel] & 0x0F) { + result = avg_ac_[channel]; + update_state_[channel] &= 0xF0; + } else { + ESP_LOGW(TAG, "No AC data for channel %d", channel); + result = NAN; + } + } else { + // ESP_LOGI(TAG, "DC: update_state_[%u]: %02X", channel, update_state_[channel]); + if (update_state_[channel] & 0xF0) { + result = avg_dc_[channel]; + update_state_[channel] &= 0x0F; + } else { + ESP_LOGW(TAG, "No DC data for channel %d", channel); + result = NAN; + } + } + xSemaphoreGive(update_avg); + } + } + return result; +} + +bool ADS131M08Hub::adc_lock(bool enable) { + int word_nbytes = adc_word_length_ >> 3; + spiframe frame(word_nbytes * 2, 0); + uint16_t command = CMD_UNLOCK; + if (enable) { + command = CMD_LOCK; + } + set_frame_word(frame, 0, command); + add_crc(frame); + write_array(frame); + delay_microseconds_safe(T_REGACQ); + read_array(frame); + auto ack = get_unsigned_frame_word(frame, 0, true); + return ack == command; +} + +/// @brief +/// @param address - register address +/// @param value - mask is used to remove unwanted bits before adding to value read from register +/// @param mask - register contents read from adc is masked with inverse of this +/// @param line - temporary / for debugging +/// @return success flag +bool ADS131M08Hub::adc_register_write_masked(uint8_t address, uint16_t value, uint16_t mask, int line) { + uint16_str reg_contents = adc_register_read(address, 1); + if (reg_contents.empty()) { + return false; + } + // ESP_LOGW(TAG, "1: reg contents: 0x%04X, address: 0x%02X, value: 0x%04X, mask: 0x%04X, caller: %d", + // reg_contents[0],address, value,mask, line ); + reg_contents[0] = (reg_contents[0] & ~mask) | (value & mask); + // ESP_LOGW(TAG, "After applying mask - reg contents: 0x%04X, caller: %d", reg_contents[0], line ); + return adc_register_write(address, reg_contents); +} +/// @brief +/// @param start_address - first register address +/// @param values - mask is used to remove unwanted bits before adding to value read from register +/// @param masks - register contents read from adc is masked with inverse of this +/// @param line - temporary / for debugging +/// @return success flag +bool ADS131M08Hub::adc_register_write_masked(uint8_t start_address, const uint16_str &values, const uint16_str &masks, + int line) { + uint8_t nregs = values.size(); + uint8_t nmasks = masks.size(); + if (nregs != nmasks) { + ESP_LOGW(TAG, + "Number register values to write (%d) are different than the corresponding number of masks (%d). Register " + "write aborted.", + nregs, nmasks); + return false; + } + uint16_str reg_contents = adc_register_read(start_address, nregs); + if (reg_contents.empty()) { + ESP_LOGW(TAG, "No contents read from adc. Expected %d values. Register write aborted.", nregs); + return false; + } + for (int i = 0; i < nregs; i++) { + // ESP_LOGW(TAG, "Before change %d: reg contents: 0x%04X, address: 0x%02X, value: 0x%04X, mask: 0x%04X, caller: %d", + // i, reg_contents[i], start_address + i, values[i], masks[i], line); + reg_contents[i] = (reg_contents[i] & ~masks[i]) | (values[i] & masks[i]); + // ESP_LOGW(TAG, "After applying mask - reg contents: 0x%04X, caller: %d", reg_contents[i], line); + } + return adc_register_write(start_address, reg_contents); +} + +bool ADS131M08Hub::adc_register_write(uint16_t address, uint16_t data) { + uint16_str arg; + arg += data; + return adc_register_write(address, arg); +} + +bool ADS131M08Hub::adc_register_write(uint16_t start_address, const uint16_str &data) { + int nregs = data.size(); + if (nregs == 0) { + return false; // invalid + } + int word_nbytes = adc_word_length_ >> 3; + int wreg_framelength = word_nbytes * (2 + nregs); // ensure room for crc + int rreg_resp_framelength = word_nbytes * (2 + (nregs == 1 ? -1 : nregs)); // add room for CRC if nregs > 1 + spiframe frame(wreg_framelength, 0); + spiframe rxframe(wreg_framelength, 0); + uint16_t addr_regcnt_mask = (start_address << 7) | ((nregs - 1) & MASK_CMD_RW_REG_COUNT); + uint16_t wreg_addr_opcode = CMD_WREG | addr_regcnt_mask; // Combine WREG command, start_address and register count + uint16_t rreg_addr_opcode = CMD_RREG | addr_regcnt_mask; // Combine RREG command, start_address and register count + bool has_mode_reg = + (start_address == REG_MODE) || ((start_address < REG_MODE) && ((start_address + nregs) > REG_MODE)); + { + CHIP_SELECT + set_frame_word(frame, 0, wreg_addr_opcode); + for (int i = 0; i < nregs; i++) { + set_frame_word(frame, i + 1, data[i]); + } + add_crc(frame); + ESP_LOGVV(TAG, "%s\n", rwreg_command_frame_to_string(frame).c_str()); + transfer_array(frame, rxframe); // WREG + } + ESP_LOGVV(TAG, "Sent Frame: %s", frame_to_string(frame).c_str()); + if (has_mode_reg) { + update_adc_word_length(); + } + delay_microseconds_safe(T_REGACQ); + auto rxdata = adc_register_read(start_address, nregs); + bool verified = !rxdata.empty(); + for (int i = 0; i < rxdata.size() && verified; i++) { + verified = rxdata[i] == data[i]; + if (!verified) { + ESP_LOGE(TAG, "Write register failed: tx data= 0x%04X, rx data= 0x%04X", data[i], rxdata[i]); + } + } + delay_microseconds_safe(T_REGACQ); + return verified; +} + +// recommended not to read more than 20 registers at a time +uint16_str ADS131M08Hub::adc_register_read(uint8_t address, uint8_t nregs) { + uint16_str result; + if (nregs == 0) { + return result; // invalid + } + if (nregs > 31) { + // We are limited to 31 registers, otherwise we will ran into DMA / SPI buffer problems + ESP_LOGE(TAG, "Trying to read %d registers. This exceeds maximum register read count of 31!", nregs); + return result; // invalid + } + int word_nbytes = adc_word_length_ >> 3; + int request_framelength = 2 * word_nbytes; // ensure room for crc + int response_framelength = word_nbytes * (2 + (nregs == 1 ? -1 : nregs)); // add room for CRC if nregs > 1 + spiframe frame(response_framelength, 0); + uint16_t rreg_addr_opcode = + CMD_RREG | (address << 7) | + ((nregs - 1) & MASK_CMD_RW_REG_COUNT); // Combine RREG command, address and register count + { + CHIP_SELECT + frame.resize(request_framelength); + set_frame_word(frame, 0, rreg_addr_opcode); + add_crc(frame); + write_array(frame); // send RREG + } + ESP_LOGVV(TAG, "%s\n", rwreg_command_frame_to_string(frame).c_str()); + ESP_LOGVV(TAG, "Sent Frame: %s", frame_to_string(frame).c_str()); + delay_microseconds_safe(T_REGACQ); + { + CHIP_SELECT + frame.resize(response_framelength); + read_array(frame); + } + ESP_LOGVV(TAG, "Recv Frame: %s", frame_to_string(frame).c_str()); + if (nregs > 1) { + bool crc_ok = check_crc(frame); + if (!crc_ok) { + ESP_LOGW(TAG, "CRC failed reading ADC registers!"); + result.clear(); + return result; // invalid + } + } + result.resize(nregs); + int offset = 0; + uint16_t rreg_ack = rreg_addr_opcode; // not really an ack, but since we don't get an ack when reading 1 register we + // pretend this is the ack + if (nregs > 1) { + offset = 1; + rreg_ack = get_unsigned_frame_word(frame, 0, true); + ; + } + // uint16_t rreg_ack = (nregs == 1) ? rreg_addr_opcode : get_unsigned_frame_word(frame, 0, true); + nregs = (rreg_ack & MASK_CMD_RW_REG_COUNT) + 1; + uint16_t start_addr = (rreg_ack & MASK_CMD_RW_REG_ADDRESS) >> 7; + for (int i = 0; i < nregs; i++) { + result[i] = get_unsigned_frame_word(frame, i + offset, true); + } + print_command_response_to_string(rreg_addr_opcode, frame); + delay_microseconds_safe(1); + return result; +} + +bool ADS131M08Hub::set_reg_osr() { + uint16_t osr = 3; // default + if (this->osr_ <= 16256 && this->osr_ >= 128) { + if (this->osr_ == 16256) + osr = 7; + else { + osr = 0; + auto tmp = this->osr_ / 128; + while (tmp >>= 1) + osr++; + } + } + return adc_register_write_masked(REG_CLOCK, osr << 2, MASK_CLOCK_OSR, __LINE__); +} + +bool ADS131M08Hub::set_channel_enable(uint8_t channel, bool enable) { + if (channel >= ADC_CHANNELS) + return false; + uint16_t enable_mask = MASK_CLOCK_CH0 << channel; + uint16_t reg_value = (enable) ? enable_mask : 0; + // ESP_LOGD(TAG, "adc_register_write_masked(REG_CLOCK=%02X, reg_value=%04X, enable_mask=%04X, __LINE__)", REG_CLOCK, + // reg_value, enable_mask); + return adc_register_write_masked(REG_CLOCK, reg_value, enable_mask, __LINE__); +} + +bool ADS131M08Hub::set_channel_gain(uint8_t channel, uint8_t gain) { + if (channel >= ADC_CHANNELS) + return false; + uint16_t reg = channel < 4 ? REG_GAIN1 : REG_GAIN2; + uint8_t shift = (channel % 4) << 2; + uint16_t mask = MASK_GAIN_PGAGAIN0 << shift; + uint16_t value = gain << shift; + // ESP_LOGW(TAG, "Ch%d: Set Gain: reg: %02X pga: %04X mask: %04X ", channel, reg, value, mask); + return adc_register_write_masked(reg, value, mask, __LINE__); +} + +void ADS131M08Hub::set_global_chop(uint16_t global_chop) { + adc_register_write_masked(REG_CFG, global_chop << 8, MASK_CFG_GC_EN, __LINE__); +} + +void ADS131M08Hub::set_global_chop_delay(uint16_t delay) { + adc_register_write_masked(REG_CFG, delay << 9, MASK_CFG_GC_DLY, __LINE__); +} + +bool ADS131M08Hub::set_channel_phase_calibration(uint8_t channel, int16_t phase_offset) { + if (channel > 7) + return false; + uint16_t reg_addr = 5 * channel + REG_CH0_CFG; + if (phase_offset < -512 || phase_offset > 511) + return false; + return adc_register_write_masked(reg_addr, (phase_offset << 6), MASK_CHX_CFG_PHASE, __LINE__); +} + +bool ADS131M08Hub::set_dcblock_filter_disable(uint8_t channel, bool disable) { + if (channel > 7) + return false; + uint16_t reg_addr = 5 * channel + REG_CH0_CFG; + uint16_t reg_value = (disable) ? MASK_CHX_CFG_DCBLKX_DIS : 0; + return adc_register_write_masked(reg_addr, reg_value, MASK_CHX_CFG_DCBLKX_DIS, __LINE__); +} + +bool ADS131M08Hub::set_channel_input_selection(uint8_t channel, ADC_INPUT_CHANNEL_MUX input) { + if (channel > 7) + return false; + uint16_t reg_addr = 5 * channel + REG_CH0_CFG; + return adc_register_write_masked(reg_addr, input, MASK_CHX_CFG_MUX, __LINE__); +} + +bool ADS131M08Hub::set_channel_offset_calibration(uint8_t channel, int32_t offset) { + if (channel > 7) + return false; + if (offset < -8388608 || offset > 8388607) + return false; + + uint16_t MSB = offset >> 8; + uint16_t LSB = offset << 8; + uint16_t reg_addr = 5 * channel + REG_CH0_OCAL_MSB; + return adc_register_write_masked(reg_addr, {MSB, LSB}, {MASK_CHX_OCAL_MSB, MASK_CHX_OCAL_LSB}, __LINE__); +} + +bool ADS131M08Hub::set_channel_gain_calibration(uint8_t channel, uint32_t gain) { + if (channel > 7) + return false; + if (gain > 16777215) + return false; + + uint16_t MSB = gain >> 8; + uint8_t LSB = gain; + uint16_t reg_addr = 5 * channel + REG_CH0_GCAL_MSB; + return adc_register_write_masked(reg_addr, {MSB, LSB}, {MASK_CHX_GCAL_MSB, MASK_CHX_GCAL_LSB}, __LINE__); +} + +// for debug only - remove references, declarations and definitions before submitting for production +std::string ADS131M08Hub::frame_to_string(const spiframe &frame) { + std::string str; + char buffer[20]; + const std::string::size_type new_cap(768); + str.reserve(new_cap); + for (int i = 0; i < frame.size(); i++) { + snprintf(buffer, sizeof(buffer), " %02X", frame[i]); + str += buffer; + } + return str; +} + +std::string ADS131M08Hub::conversion_frame_to_string(const spiframe &frame) { + std::string str; + char buffer[20]; + const std::string::size_type new_cap(768); + str.reserve(new_cap); + int word_nbytes = adc_word_length_ >> 3; + int frame_nwords = frame.size() / word_nbytes; + uint16_t drdy_status = get_unsigned_frame_word(frame, 0, true) & MASK_STATUS_DRDY; + // init i = 0 to also print status word + for (int i = 1; i < frame_nwords - 1; i++) { + if (i == 0) { + for (int j = 0; j < word_nbytes; j++) { + snprintf(buffer, sizeof(buffer), "%02X", frame[j]); + str += buffer; + } + } else { + if (drdy_status & 1) { + str += std::format(" CH{}:", i - 1); + for (int j = 0; j < word_nbytes; j++) { + snprintf(buffer, sizeof(buffer), "%02X", frame[i * word_nbytes + j]); + str += buffer; + } + } + drdy_status = drdy_status >> 1; + } + } + return str; +} + +std::string ADS131M08Hub::rwreg_command_frame_to_string(const spiframe &frame) { + char buffer[100]; + std::string str; + str.reserve(512); + uint16_t cmdadr = get_unsigned_frame_word(frame, 0, true); + uint16_t address = (cmdadr & MASK_CMD_RW_REG_ADDRESS) >> 7; + int nregs = 1 + (cmdadr & MASK_CMD_RW_REG_COUNT); + str = command_to_string(cmdadr); + uint16_t command = (cmdadr & MASK_CMD_RW_REG) ? (cmdadr & MASK_CMD_RW_REG) : cmdadr; + const char *ws = " \t\n\r\f\v"; // Defines whitespace + if (command == CMD_RREG) { + str += " : regs "; + for (int i = 0; i < nregs; i++) { + str += reg_addr_to_string(address + i); + str.erase(str.find_last_not_of(ws) + 1); + str += (i < (nregs - 1) ? ", " : ""); + } + } + if (command == CMD_WREG) { + str += ", data:"; + for (int i = 0; i < nregs; i++) { + uint16_t data = get_unsigned_frame_word(frame, i + 1, true); + snprintf(buffer, sizeof(buffer), " 0x%04X", data); + str += buffer; + } + for (int i = 0; i < nregs; i++) { + uint16_t data = get_unsigned_frame_word(frame, i + 1, true); + str += "\n "; + str += reg_data_to_string(address + i, data); + str.erase(str.find_last_not_of(ws) + 1); + str += (i < (nregs - 1) ? ", " : ""); + } + } + return str; +} + +std::string ADS131M08Hub::reg_addr_to_string(int address) { return reg_data_to_string(address, 0, true); } + +std::string ADS131M08Hub::reg_data_to_string(int address, uint16_t data, bool nameonly) { + std::string str; + char buffer[100]; + str.reserve(120); + switch (address) { + case REG_ID: + str += std::format("{:<10}", "ID"); + if (nameonly) + break; + snprintf(buffer, sizeof(buffer), ": %d ", (data >> 8) & 0x0F); + str += buffer; + break; + case REG_STATUS: + str += std::format("{:<10}", "STATUS"); + if (nameonly) + break; + str += ":"; + str += status_to_string(data); + break; + case REG_MODE: + str += std::format("{:<10}", "MODE"); + if (nameonly) + break; + str += ":"; + if ((data & MASK_MODE_REG_CRC_EN) != 0) + str += " REGMAP_CRC_EN /"; + if ((data & MASK_MODE_RX_CRC_EN) != 0) + str += " SPI_IN_CRC_EN /"; + if ((data & MASK_MODE_TIMEOUT) != 0) + str += " SPI_TIMEOUT_EN /"; + str += " CRC="; + str += ((data & MASK_MODE_CRC_TYPE) != 0) ? "ANSI /" : "CCITT /"; + if ((data & MASK_MODE_RESET) != 0) + str += " RESET /"; + str += " WL="; + if ((data & MASK_MODE_WLENGTH) == WLENGTH_16_BITS) + str += "16"; + if ((data & MASK_MODE_WLENGTH) == WLENGTH_24_BITS) + str += "24"; + if ((data & MASK_MODE_WLENGTH) == WLENGTH_32_BITS_LSB_ZERO_PADDING) + str += "32zp"; + if ((data & MASK_MODE_WLENGTH) == WLENGTH_32_BITS_MSB_SIGN_EXTEND) + str += "32se"; + str += " / DRDY SEL="; + if ((data & MASK_MODE_DRDY_SEL) == DRDY_SEL_MOST_LAGGING) + str += "MOST_LAGGING"; + if ((data & MASK_MODE_DRDY_SEL) == DRDY_SEL_LOGICAL_OR) + str += "OR_OF_CHANLS"; + if ((data & MASK_MODE_DRDY_SEL) == DRDY_SEL_MOST_LEADING_CHAN) + str += "MOST_LEADING"; + if ((data & MASK_MODE_DRDY_SEL) == DRDY_SEL_MOST_LEADING_CHAN2) + str += "MOST_LEADING"; + str += " / DRDY IDLE="; + str += ((data & MASK_MODE_DRDY_HiZ) != 0) ? "Hi_IMP" : "LOGIC_HI"; + str += " / DRDY ACTV="; + str += ((data & MASK_MODE_DRDY_FMT) != 0) ? "LOW_FIX_DUR" : "LOGIC_LOW"; + break; + case REG_CLOCK: + str += std::format("{:<10}", "CLOCK"); + if (nameonly) + break; + str += ": CH ENABLED="; + str += ((data & MASK_CLOCK_CH7) != 0) ? "7" : "_"; + str += ((data & MASK_CLOCK_CH6) != 0) ? "6" : "_"; + str += ((data & MASK_CLOCK_CH5) != 0) ? "5" : "_"; + str += ((data & MASK_CLOCK_CH4) != 0) ? "4" : "_"; + str += ((data & MASK_CLOCK_CH3) != 0) ? "3" : "_"; + str += ((data & MASK_CLOCK_CH2) != 0) ? "2" : "_"; + str += ((data & MASK_CLOCK_CH1) != 0) ? "1" : "_"; + str += ((data & MASK_CLOCK_CH0) != 0) ? "0" : "_"; + str += " / XTAL OSC="; + str += ((data & MASK_CLOCK_XTAL_DIS) != 0) ? "DIS" : "EN"; + str += " / EXT VREF="; + str += ((data & MASK_CLOCK_EXTREF_EN) != 0) ? "EN" : "DIS"; + { + uint16_t osr = 1 << (7 + ((data & MASK_CLOCK_OSR) >> 2)); + if (osr == 16384) { + osr = 16256; + } + snprintf(buffer, sizeof(buffer), " / OSR=%d", osr); + str += buffer; + } + str += " / POWER="; + if ((data & MASK_CLOCK_PWR) == PM_VERY_LOW_POWER) + str += "VERY_LOW"; + if ((data & MASK_CLOCK_PWR) == PM_LOW_POWER) + str += "VERY_LOW"; + if ((data & MASK_CLOCK_PWR) == PM_HIGH_RESOLUTION) + str += "HIGH_RES"; + if ((data & MASK_CLOCK_PWR) == PM_HIGH_RESOLUTION2) + str += "HIGH_RES"; + break; + + case REG_GAIN1: { + str += std::format("{:<10}", "GAIN1"); + if (nameonly) + break; + auto gain = 1 << ((data & MASK_GAIN_PGAGAIN3) >> 24); + snprintf(buffer, sizeof(buffer), ": Ch3=%d /", gain); + str += buffer; + gain = 1 << ((data & MASK_GAIN_PGAGAIN2) >> 16); + snprintf(buffer, sizeof(buffer), " Ch2=%d /", gain); + str += buffer; + gain = 1 << ((data & MASK_GAIN_PGAGAIN1) >> 8); + snprintf(buffer, sizeof(buffer), " Ch1=%d /", gain); + str += buffer; + gain = 1 << (data & MASK_GAIN_PGAGAIN0); + snprintf(buffer, sizeof(buffer), " Ch0=%d", gain); + str += buffer; + break; + } + + case REG_GAIN2: { + str += std::format("{:<10}", "GAIN2"); + if (nameonly) + break; + auto gain = 1 << ((data & MASK_GAIN_PGAGAIN7) >> 24); + snprintf(buffer, sizeof(buffer), ": Ch7=%d /", gain); + str += buffer; + gain = 1 << ((data & MASK_GAIN_PGAGAIN6) >> 16); + snprintf(buffer, sizeof(buffer), " Ch6=%d /", gain); + str += buffer; + gain = 1 << ((data & MASK_GAIN_PGAGAIN5) >> 8); + snprintf(buffer, sizeof(buffer), " Ch5=%d /", gain); + str += buffer; + gain = 1 << (data & MASK_GAIN_PGAGAIN4); + snprintf(buffer, sizeof(buffer), " Ch4=%d", gain); + str += buffer; + break; + } + case REG_CFG: { + str += std::format("{:<10}", "CFG"); + if (nameonly) + break; + str += ": GC_EN="; + str += ((data & MASK_CFG_GC_EN) != 0) ? "Y /" : "N /"; + auto gc_dly = 1 << (1 + ((data & MASK_CFG_GC_DLY) >> 9)); + snprintf(buffer, sizeof(buffer), " GC DELAY= %d", gc_dly); + str += buffer; + str += " / CUR DET= ["; + str += ((data & MASK_CFG_CD_EN) != 0) ? "Y] " : "N] "; + str += ((data & MASK_CFG_CD_ALLCH) != 0) ? "ALL_CH" : "ANY_CH"; + auto cd_num = 1 << ((data & MASK_CFG_CD_NUM) >> 4); + snprintf(buffer, sizeof(buffer), " / CD_NUM= %d", cd_num); + str += buffer; + auto cd_len = (data & MASK_CFG_CD_LEN) >> 1; + switch (cd_len) { + case 0: + cd_len = 128; + break; + case 1: + cd_len = 256; + break; + case 2: + cd_len = 512; + break; + case 3: + cd_len = 768; + break; + case 4: + cd_len = 1280; + break; + case 5: + cd_len = 1792; + break; + case 6: + cd_len = 2560; + break; + case 7: + cd_len = 3584; + break; + default: + cd_len = 0; // illegal case + break; + } + snprintf(buffer, sizeof(buffer), " / CD_LEN= %d", cd_len); + str += buffer; + break; + } + case REG_THRSHLD_MSB: + str += std::format("{:<10}", "TH_MSB"); + if (nameonly) + break; + snprintf(buffer, sizeof(buffer), ": 0x%04X", data); + str += buffer; + break; + + case REG_THRSHLD_LSB: + str += std::format("{:<10}", "TH_LSB"); + if (nameonly) + break; + snprintf(buffer, sizeof(buffer), ": 0x%04X / ", (data & MASK_THRSHLD_LSB_CD_TH_LSB) >> 8); + str += buffer; + snprintf(buffer, sizeof(buffer), " DCBLOCK: 0x%04X", data & MASK_THRSHLD_LSB_DCBLOCK); + str += buffer; + break; + + case REGMAP_CRC: + str += std::format("{:<10}", std::format("REGMAP_CRC[{:#02}]", address)); + if (nameonly) + break; + snprintf(buffer, sizeof(buffer), " value: 0x%04X", data); + str += buffer; + + default: + if (address >= REG_CH0_CFG && address <= REG_CH7_GCAL_LSB) { + str += reg_config_to_string(address, data, nameonly); + break; + } + } + return str; +} + +std::string ADS131M08Hub::reg_config_to_string(int address, uint16_t data, bool nameonly) { + std::string str; + str.reserve(256); + int ch_num = (address - REG_CH0_CFG) / 5; + int gen_addr = (address - REG_CH0_CFG) % 5; + std::string ch_str = std::format("CH{}_", ch_num); + switch (gen_addr) { + case REG_CHX_CFG: + str += std::format("{:<10}", std::format("{}CFG", ch_str)); + if (nameonly) + break; + str += std::format(": PH_DEL={0:}", (data & MASK_CHX_CFG_PHASE) >> 6); + str += " DCBLOCK_EN="; + str += ((data & MASK_CHX_CFG_DCBLKX_DIS) != 0) ? "N /" : "Y /"; + str += std::format("{}INPUT=", ch_str); + if ((data & MASK_CHX_CFG_MUX) == ICM_AIN0P_AIN0N) + str += "AIN0P and AIN0N"; + if ((data & MASK_CHX_CFG_MUX) == ICM_INPUT_SHORTED) + str += "ADC inputs shorted"; + if ((data & MASK_CHX_CFG_MUX) == ICM_POSITIVE_DC_TEST_SIGNAL) + str += "Positive DC test signal"; + if ((data & MASK_CHX_CFG_MUX) == ICM_NEGATIVE_DC_TEST_SIGNAL) + str += "Negative DC test signal"; + break; + + case REG_CHX_OCAL_MSB: + str += std::format("{:<13}", std::format("{}OCAL_MSB", ch_str)); + if (nameonly) + break; + str += std::format(": {:#04x}", data); + break; + + case REG_CHX_OCAL_LSB: + str += std::format("{:<13}", std::format("{}OCAL_LSB", ch_str)); + if (nameonly) + break; + str += std::format(": {:#04x}", (data & MASK_CHX_OCAL_LSB) >> 8); + break; + + case REG_CHX_GCAL_MSB: + str += std::format("{:<13}", std::format("{}GCAL_MSB", ch_str)); + if (nameonly) + break; + str += std::format(": {:#04x}", data); + break; + + case REG_CHX_GCAL_LSB: + str += std::format("{:<13}", std::format("{}GCAL_LSB", ch_str)); + if (nameonly) + break; + str += std::format(": {:#04x}", (data & MASK_CHX_GCAL_LSB) >> 8); + break; + } + return str; +} + +std::string ADS131M08Hub::command_to_string(uint16_t cmdadr) { + std::string str; + uint16_t reg_addr, nregs; + char buffer[40]; + const std::string::size_type new_cap(256); + str.reserve(new_cap); + snprintf(buffer, sizeof(buffer), "0x%04X", cmdadr); + uint16_t cmd_sw = (cmdadr & MASK_CMD_RW_REG) ? (cmdadr & MASK_CMD_RW_REG) : cmdadr; + switch (cmd_sw) { + case CMD_NULL: + str = "NULL ["; + str += buffer; + str += "]"; + break; + case CMD_RESET: + str = "RESET ["; + str += buffer; + str += "]"; + break; + case CMD_STANDBY: + str = "STANDBY ["; + str += buffer; + str += "]"; + break; + case CMD_WAKEUP: + str = "WAKEUP ["; + str += buffer; + str += "]"; + break; + case CMD_LOCK: + str = "LOCK ["; + str += buffer; + str += "]"; + break; + case CMD_UNLOCK: + str = "UNLOCK ["; + str += buffer; + str += "]"; + break; + case CMD_RREG: + str = "RREG ["; + str += buffer; + str += "]: "; + nregs = cmdadr & MASK_CMD_RW_REG_COUNT; + nregs++; + reg_addr = (cmdadr & MASK_CMD_RW_REG_ADDRESS) >> 7; + snprintf(buffer, sizeof(buffer), "#regs:%d, start_addr:%d ", nregs, reg_addr); + str += buffer; + break; + case CMD_WREG: + str = "WREG ["; + str += buffer; + str += "]: "; + nregs = cmdadr & MASK_CMD_RW_REG_COUNT; + nregs++; + reg_addr = (cmdadr & MASK_CMD_RW_REG_ADDRESS) >> 7; + snprintf(buffer, sizeof(buffer), "#regs:%d, start_addr:%d ", nregs, reg_addr); + str += buffer; + break; + default: + str = "UNKNOWN command ["; + str += buffer; + str += "]"; + break; + } + return str; +} +// we do not return strings from this function as its length could exceed maximum length that log print can handle +void ADS131M08Hub::print_command_response_to_string(uint16_t cmdadr_sent, const spiframe &frame) { + std::string str; + uint16_t nregs, start_addr, respcmd; + char buffer[40]; + str.reserve(256); + uint16_t response = frame[0] << 8 | frame[1]; + snprintf(buffer, sizeof(buffer), " [0x%04X]: ", response); + std::string resp_txt(buffer); + uint16_t command = (cmdadr_sent & MASK_CMD_RW_REG) ? (cmdadr_sent & MASK_CMD_RW_REG) : cmdadr_sent; + switch (command) { + case CMD_NULL: + str = "STATUS" + resp_txt; + str += status_to_string(response); + ESP_LOGD(TAG, "%s", str.c_str()); + break; + case CMD_RESET: + str = "RESET RESPONSE" + resp_txt; + switch (response) { + case RSP_RESET_OK: + str += "RESET SUCCESS"; + break; + case RSP_RESET_NOK: + str += "RESET NOT COMPLETE"; + break; + default: + str += "UNKNOWN RESET RESPONSE"; + break; + } + ESP_LOGD(TAG, "%s", str.c_str()); + break; + case CMD_STANDBY: + str = "STANDBY RESPONSE" + resp_txt; + if (response == CMD_STANDBY) { + str += "IN STANDBY"; + } else { + str += "UNKNOWN"; + } + ESP_LOGD(TAG, "%s", str.c_str()); + break; + case CMD_WAKEUP: + str = "WAKEUP RESPONSE" + resp_txt; + if (response == CMD_WAKEUP) { + str += "EXITED STANDBY"; + } else { + str += "UNKNOWN"; + } + ESP_LOGD(TAG, "%s", str.c_str()); + break; + case CMD_LOCK: + str = "LOCK RESPONSE " + resp_txt; + if (response == CMD_LOCK) { + str += "LOCKED"; + } else { + str += "UNKNOWN"; + } + ESP_LOGD(TAG, "%s", str.c_str()); + break; + case CMD_UNLOCK: + str = "UNLOCK RESPONSE" + resp_txt; + if (response == CMD_UNLOCK) { + str += "UNLOCKED"; + } else { + str += "UNKNOWN"; + } + ESP_LOGD(TAG, "%s", str.c_str()); + break; + case CMD_RREG: { + int offset = 0; + str = "RREG RESPONSE" + resp_txt; + nregs = (cmdadr_sent & MASK_CMD_RW_REG_COUNT) + 1; + uint16_t rreg_ack = cmdadr_sent; // not really and ack, but since we don't get an ack when reading 1 register we + // pretend this is the ack + if (nregs > 1) { + offset = 1; + rreg_ack = get_unsigned_frame_word(frame, 0, true); + ; + nregs = (rreg_ack & MASK_CMD_RW_REG_COUNT) + + 1; // should not change here, but just in case adc decided to sent different number of registers + } + start_addr = (rreg_ack & MASK_CMD_RW_REG_ADDRESS) >> 7; + snprintf(buffer, sizeof(buffer), "#regs:%d, start_addr:%d ", nregs, start_addr); + ESP_LOGVV(TAG, "%s", buffer); + if (nregs == 1) { + start_addr = (cmdadr_sent & MASK_CMD_RW_REG_ADDRESS) >> 7; + str = reg_data_to_string(start_addr, response); + ESP_LOGVV(TAG, "%s", str.c_str()); + } else { + respcmd = response & MASK_CMD_RW_REG_RESP; + if (respcmd == RREG_RESP) { + for (int i = 0; i < nregs; i++) { + uint16_t rxdata = get_unsigned_frame_word(frame, i + offset, true); + str = reg_data_to_string(start_addr + i, rxdata); + ESP_LOGVV(TAG, "%s", str.c_str()); + } + } else { + str = "WARNING: invalid response to RREG. Should be Exxxx or Fxxxx"; + ESP_LOGW(TAG, "%s", str.c_str()); + } + } + } break; + case CMD_WREG: + str = "WREG RESPONSE" + resp_txt; + respcmd = response & MASK_CMD_RW_REG_RESP; + if (respcmd == WREG_RESP) { + nregs = response & MASK_CMD_RW_REG_COUNT; + start_addr = (response & MASK_CMD_RW_REG_ADDRESS) >> 7; + snprintf(buffer, sizeof(buffer), "#regs:%d, start-addr:%d ", nregs + 1, start_addr); + str += buffer; + ESP_LOGVV(TAG, "%s", str.c_str()); + } else { + str += "WARNING: invalid response to WREG. Should be 4xxxx or 5xxxx"; + ESP_LOGW(TAG, "%s", str.c_str()); + } + break; + default: + str = "Response to UNKNOWN command ["; + str += buffer; + str += "]"; + ESP_LOGW(TAG, "%s", str.c_str()); + break; + } +} + +std::string ADS131M08Hub::status_to_string(uint16_t response) { + std::string str; + str.reserve(90); + if ((response & MASK_STATUS_LOCK) != 0) + str += " LOCK /"; + if ((response & MASK_STATUS_RESYNC) != 0) + str += " RESYNC /"; + if ((response & MASK_STATUS_REGMAP) != 0) + str += " REGMAP /"; + if ((response & MASK_STATUS_CRC_ERR) != 0) + str += " CRC_ERR /"; + str += " CRC="; + str += ((response & MASK_STATUS_CRC_TYPE) != 0) ? "ANSI /" : "CCITT /"; + if ((response & MASK_STATUS_RESET) != 0) + str += " RESET /"; + str += " WL="; + if ((response & MASK_STATUS_WLENGTH) == WLENGTH_16_BITS) + str += "16"; + if ((response & MASK_STATUS_WLENGTH) == WLENGTH_24_BITS) + str += "24"; + if ((response & MASK_STATUS_WLENGTH) == WLENGTH_32_BITS_LSB_ZERO_PADDING) + str += "32zp"; + if ((response & MASK_STATUS_WLENGTH) == WLENGTH_32_BITS_MSB_SIGN_EXTEND) + str += "32se"; + str += " / DRDY="; + str += ((response & MASK_STATUS_DRDY7) != 0) ? "7" : "_"; + str += ((response & MASK_STATUS_DRDY6) != 0) ? "6" : "_"; + str += ((response & MASK_STATUS_DRDY5) != 0) ? "5" : "_"; + str += ((response & MASK_STATUS_DRDY4) != 0) ? "4" : "_"; + str += ((response & MASK_STATUS_DRDY3) != 0) ? "3" : "_"; + str += ((response & MASK_STATUS_DRDY2) != 0) ? "2" : "_"; + str += ((response & MASK_STATUS_DRDY1) != 0) ? "1" : "_"; + str += ((response & MASK_STATUS_DRDY0) != 0) ? "0" : "_"; + return str; +} + +// end of for debug only section + +} // namespace ads131m08 +} // namespace esphome diff --git a/ads131m08/ads131m08.h b/ads131m08/ads131m08.h new file mode 100644 index 0000000..7bdf5c3 --- /dev/null +++ b/ads131m08/ads131m08.h @@ -0,0 +1,196 @@ +#pragma once +// using external voltage reference + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/optional.h" +#include "ads131m08_defs.h" +#include "uint_str.h" +#include +#include +#include + +#include "esphome/components/spi/spi.h" +#include "esphome/components/sensor/sensor.h" +// +// #include +// #include +// #include +// #include +// #include + +namespace esphome { +namespace ads131m08 { + +static const uint8_t DEFAULT_WORD_LENGTH = 32; +static const int MAX_CHANNELS = 12; // for debugging +static const int ADC_CHANNELS = 8; +static const int SAMPLE_TIME_SENSOR = 8; // for debugging +static const int MAX_SAMPLES_SENSOR = 9; // for debugging +static const int CRC_ERRORS_SENSOR = 10; // for debugging +static const int ISR_COUNT_SENSOR = 11; // for debugging + +// #define SET_IRAM IRAM_ATTR +#define SET_IRAM + +typedef std::vector spiframe; +typedef uint_str::Ty_string uint16_str; // we want to use the stack to pass small arrays + +class ADS131M08Hub : public Component, + public spi::SPIDevice { + friend class ads131m08_select; + + public: + // register config values used + static constexpr uint16_t reg_clock_cfg = MASK_CLOCK_EXTREF_EN | OSR_1024 | PM_HIGH_RESOLUTION; + static constexpr uint16_t reg_clock_allch_on = MASK_CLOCK_ALLCH | reg_clock_cfg; + static constexpr uint16_t reg_clock_allch_off = ~MASK_CLOCK_ALLCH & reg_clock_cfg; + static constexpr uint16_t reg_mode_cfg16 = WLENGTH_16_BITS | DRDY_FMT_PULSE | TIMEOUT_ENABLED; + static constexpr uint16_t reg_mode_cfg24 = WLENGTH_24_BITS | DRDY_FMT_PULSE | TIMEOUT_ENABLED; + static constexpr uint16_t reg_mode_cfg32 = WLENGTH_32_BITS_MSB_SIGN_EXTEND | DRDY_FMT_PULSE | TIMEOUT_ENABLED; + // masks for the above + static constexpr uint16_t reg_clock_cfg_mask = MASK_CLOCK_EXTREF_EN | MASK_CLOCK_OSR | MASK_CLOCK_PWR; + static constexpr uint16_t reg_clock_allch_mask = MASK_CLOCK_ALLCH_OFF | reg_clock_cfg_mask; + static constexpr uint16_t reg_mode_cfg_mask = MASK_MODE_WLENGTH | MASK_MODE_DRDY_FMT | MASK_MODE_TIMEOUT; + // Semaphore handles + SemaphoreHandle_t data_ready_semhandle = NULL; + SemaphoreHandle_t update_avg = NULL; + + const int numFrameWords = 10; // Number of words in a full ADS131M08 SPI frame + bool firstRead = true; // Flag to tell us if we are reading ADC data for the first time + // signed long adcData; // Location where DMA will store ADC data in memory, length defined elsewhere + // ADS131M08Hub(); + void txf_init(); + bool adc_initialize(uint8_t word_length); + bool adc_set_word_length(uint8_t word_length); + + void setup() override; + void loop() override; + void set_drdy_pin(InternalGPIOPin *pin) { drdy_pin_ = pin; } + void set_sync_reset_pin(InternalGPIOPin *pin) { sync_reset_pin_ = pin; } + void set_clock_frequency(float clock_frequency) { this->clock_frequency_ = clock_frequency; } + void register_sensor_ac(int channel, sensor::Sensor *s) { sensors_ac[channel] = s; } + void register_sensor_dc(int channel, sensor::Sensor *s) { sensors_dc[channel] = s; } + void set_reference_voltage(float reference_voltage) { this->reference_voltage_ = reference_voltage; } + void set_osr(uint16_t osr) { this->osr_ = osr; } + void dump_config() override; + + bool set_channel_enable(uint8_t channel, bool enable); + void set_global_chop(uint16_t global_chop); + void set_global_chop_delay(uint16_t delay); + bool set_channel_input_selection(uint8_t channel, ADC_INPUT_CHANNEL_MUX input); + bool set_channel_offset_calibration(uint8_t channel, int32_t offset); + bool set_channel_gain_calibration(uint8_t channel, uint32_t gain); + bool set_channel_phase_calibration(uint8_t channel, int16_t phase_offset); + bool set_dcblock_filter_disable(uint8_t channel, bool disable); + bool set_channel_gain(uint8_t channel, uint8_t gain); + bool set_measure_rms(uint8_t channel, bool enable); + float get_sampled_value(uint8_t channel) { return sampled_values_[channel]; } + float get_average(uint8_t channel, bool read_ac); + bool set_reg_osr(); + + protected: + HighFrequencyLoopRequester high_freq_; + static void IRAM_ATTR isr_handler(ADS131M08Hub *arg); + float reference_voltage_; + float clock_frequency_; + InternalGPIOPin *drdy_pin_; + InternalGPIOPin *sync_reset_pin_ = {nullptr}; + sensor::Sensor *sensors_ac[MAX_CHANNELS] = {nullptr}; + sensor::Sensor *sensors_dc[MAX_CHANNELS] = {nullptr}; + float sampled_values_[ADC_CHANNELS]; + bool rms_enabled_[MAX_CHANNELS]; + bool rms_calc_req_{false}; + uint16_t osr_{3}; + uint8_t update_adc_word_length(); + uint8_t update_adc_word_length(uint16_t status); + void SET_IRAM read_single(); + std::pair SET_IRAM read_multi(); + bool adc_lock(bool enable); + bool adc_register_write(uint16_t address, uint16_t data); + bool adc_register_write(uint16_t address, const uint16_str &data); + uint16_str adc_register_read(uint8_t address, uint8_t nregs); + bool adc_register_write_masked(uint8_t address, uint16_t value, uint16_t mask, int line); + bool adc_register_write_masked(uint8_t start_address, const uint16_str &values, const uint16_str &masks, int line); + bool adc_reset_retry(); + bool adc_soft_reset(); + void adc_hard_reset(); + void adc_sync(); + void write_byte(uint8_t byte); + bool write(uint32_t data, uint8_t adcWordLength); + uint8_t read_byte(); + void write_array(const spiframe &data); + void SET_IRAM read_array(spiframe &buffer); + void transfer_array(const spiframe &data_out, spiframe &data_in); + uint16_t SET_IRAM get_crc(const spiframe &frame); + bool SET_IRAM check_crc(const spiframe &frame_with_crc); + size_t SET_IRAM add_crc(spiframe &frame); + uint16_t SET_IRAM read_frame_crc(const spiframe &frame); + uint16_t SET_IRAM crc(uint16_t crc_register, uint8_t data); + bool SET_IRAM set_frame_word(spiframe &frame, int w_index, + uint16_t data); // write 16 bit data to first two bytes of word + bool SET_IRAM set_frame_word(spiframe &frame, int w_index, uint32_t data); + uint32_t SET_IRAM get_unsigned_frame_word(const spiframe &frame, int w_index, bool force_16bits = false); + int32_t SET_IRAM get_sign_ext_frame_word(const spiframe &frame, int w_index); + bool update_averages(uint8_t channel, float rms_ac, float rms_dc); + std::atomic adc_init_{0}; + std::atomic cs_ctr_{ + 0}; // Counter to track nested CS enable/disable calls for proper handling of multiple transfers in a row + std::atomic isr_ctr_{0}; + void enable(const char *txt); + void disable(const char *txt); + struct chipselect { + ADS131M08Hub *parent_{nullptr}; + const char *txt_; + chipselect(ADS131M08Hub *parent, const char *txt = nullptr) { + parent_ = parent; + if (parent_ != nullptr) + txt_ = txt; + parent_->enable(txt_); + } + ~chipselect() { + if (parent_ != nullptr) { + parent_->disable(txt_); + parent_ = nullptr; + } + } + }; + float update_conversion_factor(); + + private: + uint8_t update_state_[MAX_CHANNELS] = {0, 0, 0, 0, 0, 0, 0, 0}; + uint8_t adc_word_length_{24}; + float conversion_factor_{1.2 / 8388608.0}; // updated when word length changes + const uint32_t sample_time_ = 40000; // 250 ms + float sps_{0.0f}; + uint32_t num_samples_[ADC_CHANNELS]; + int64_t sample_sum_[ADC_CHANNELS]; + float sample_squared_sum_[ADC_CHANNELS]; + float avg_dc_[MAX_CHANNELS]; + float avg_ac_[MAX_CHANNELS]; + // for debug only + std::string frame_to_string(const spiframe &frame); + std::string conversion_frame_to_string(const spiframe &frame); + std::string command_to_string(uint16_t cmdadr); + void print_command_response_to_string(uint16_t cmdadr_sent, const spiframe &frame); + std::string rwreg_command_frame_to_string(const spiframe &frame); + std::string status_to_string(uint16_t response); + std::string reg_data_to_string(int address, uint16_t data, bool nameonly = false); + std::string reg_addr_to_string(int address); + std::string reg_config_to_string(int address, uint16_t data, bool nameonly = false); + // temp + int first_read_data{0}; +}; + +#ifndef CHIP_SELECT +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE +#define CHIP_SELECT volatile chipselect cs(this, __FUNCTION__); +#else +#define CHIP_SELECT volatile chipselect cs(this); +#endif +#define CHIP_SELECTx volatile chipselect cs(this); +#endif + +} // namespace ads131m08 +} // namespace esphome diff --git a/ads131m08/ads131m08_defs.h b/ads131m08/ads131m08_defs.h new file mode 100644 index 0000000..1b55eb0 --- /dev/null +++ b/ads131m08/ads131m08_defs.h @@ -0,0 +1,358 @@ +#pragma once + +#include + +namespace esphome { +namespace ads131m08 { + +static constexpr size_t MAX_FRAME_SIZE = + 50; // in frame_words. This is an arbitrary limit, if frame is above this size something must have gone wrong + +enum ADC_DRDY_STATE : uint8_t { + DS_LOGIC_HIGH = 0, // DEFAULT + DS_HI_Z = 1 +}; + +enum ADC_POWERMODE : uint16_t { + PM_VERY_LOW_POWER = 0, + PM_LOW_POWER = 1, + PM_HIGH_RESOLUTION = 2, // DEFAULT + PM_HIGH_RESOLUTION2 = 3, +}; + +enum ADC_PGA_GAIN : uint8_t { + PGA_1 = 0, + PGA_2 = 1, + PGA_4 = 2, + PGA_8 = 3, + PGA_16 = 4, + PGA_32 = 5, + PGA_64 = 6, + PGA_128 = 7, + PGA_INVALID +}; + +enum ADC_INPUT_CHANNEL_MUX : uint8_t { + ICM_AIN0P_AIN0N = 0, // DEFAULT + ICM_INPUT_SHORTED = 1, + ICM_POSITIVE_DC_TEST_SIGNAL = 2, + ICM_NEGATIVE_DC_TEST_SIGNAL = 3, +}; + +enum ADC_OVERSAMPLING_RATIO : uint16_t { + OSR_128 = 0, + OSR_256 = 1, + OSR_512 = 2, + OSR_1024 = 3, // default + OSR_2048 = 4, + OSR_4096 = 5, + OSR_8192 = 6, + OSR_16256 = 7 +}; + +enum ADC_WAIT_TIME : uint16_t { + WT_128 = 856, + WT_256 = 1112, + WT_512 = 1624, + WT_1024 = 2648, + WT_2048 = 4696, + WT_4096 = 8792, + WT_8192 = 16984, + WT_16384 = 33368 +}; + +// delays in microseconds +enum ADC_DELAY { + T_REGACQ = 5, + +}; + +// MODE Register +// enum ADC_RESET : uint16_t +//{ +static constexpr uint16_t MODE_MASK_RESET_HAPPENED = ~0x0400; // DEFAULT +static constexpr uint16_t MODE_RESET_HAPPENED = 0x0400; +//}; + +enum ADC_CRC_TYPE : uint16_t { + CRC_CCITT_16BIT = 0x0000, // DEFAULT + CRC_ANSI_16BIT = 0x0800 +}; + +// enum ADC_WORD_LENGTH : uint16_t +//{ +static constexpr uint16_t WLENGTH_16_BITS = 0x0000; +static constexpr uint16_t WLENGTH_24_BITS = 0x0100; // DEFAULT +static constexpr uint16_t WLENGTH_32_BITS_LSB_ZERO_PADDING = 0x0200; +static constexpr uint16_t WLENGTH_32_BITS_MSB_SIGN_EXTEND = 0x0300; +//}; + +enum ADC_TIMEOUT : uint16_t { + TIMEOUT_DISABLED = 0x0000, + TIMEOUT_ENABLED = 0x0010 // DEFAULT +}; + +enum ADC_DRDY_SELECTION : uint16_t { + DRDY_SEL_MOST_LAGGING = 0x0000, // DEFAULT + DRDY_SEL_LOGICAL_OR = 0x0004, + DRDY_SEL_MOST_LEADING_CHAN = 0x0008, + DRDY_SEL_MOST_LEADING_CHAN2 = 0x000C +}; +// enum ADC_DRDY_FORMAT : uint16_t +//{ +static constexpr uint16_t DRDY_FMT_LEVEL = 0x0000; // Logic low (default) +static constexpr uint16_t DRDY_FMT_PULSE = 0x0001; // Low pulse with a fixed duration +//}; +// end of MODE Register + +// Commands +enum ADC_COMMANDS : uint16_t { + CMD_NULL = 0x0000, // No operation; used to read STATUS register + CMD_RESET = 0x0011, // RESET the device + CMD_STANDBY = 0x0022, // Place the device into standby mode + CMD_WAKEUP = 0x0033, // Wake up device from standby mode to conversion mode + CMD_LOCK = 0x0555, // Lock the interface such that only the NULL, UNLOCK, and RREG commands are valid + CMD_UNLOCK = 0x0655, // Unlock the interface after the interface is locked + CMD_RREG = 0xA000, // Read register command base; number of registers to read added to lower byte; register address + // to upper byte + CMD_WREG = 0x6000, // Write register command base; number of registers to write added to lower byte; register address + // to upper byte +}; + +// Responses +enum ADC_RESPONSES : uint16_t { RSP_RESET_OK = 0xFF28, RSP_RESET_NOK = 0x0011 }; + +// Used to generalise register config addresses +enum REGRELADDR : uint16_t { + REG_CHX_CFG, + REG_CHX_OCAL_MSB, + REG_CHX_OCAL_LSB, + REG_CHX_GCAL_MSB, + REG_CHX_GCAL_LSB, +}; + +// enum ADC_REG : uint16_t { +static constexpr uint16_t REG_ID = 0x00; +static constexpr uint16_t REG_STATUS = 0x01; +static constexpr uint16_t REG_MODE = 0x02; +static constexpr uint16_t REG_CLOCK = 0x03; +static constexpr uint16_t REG_GAIN1 = 0x04; +static constexpr uint16_t REG_GAIN2 = 0x05; +static constexpr uint16_t REG_CFG = 0x06; +static constexpr uint16_t REG_THRSHLD_MSB = 0x07; +static constexpr uint16_t REG_THRSHLD_LSB = 0x08; +static constexpr uint16_t REG_CH0_CFG = 0x09; +static constexpr uint16_t REG_CH0_OCAL_MSB = 0x0A; +static constexpr uint16_t REG_CH0_OCAL_LSB = 0x0B; +static constexpr uint16_t REG_CH0_GCAL_MSB = 0x0C; +static constexpr uint16_t REG_CH0_GCAL_LSB = 0x0D; +static constexpr uint16_t REG_CH1_CFG = 0x0E; +static constexpr uint16_t REG_CH1_OCAL_MSB = 0x0F; +static constexpr uint16_t REG_CH1_OCAL_LSB = 0x10; +static constexpr uint16_t REG_CH1_GCAL_MSB = 0x11; +static constexpr uint16_t REG_CH1_GCAL_LSB = 0x12; +static constexpr uint16_t REG_CH2_CFG = 0x13; +static constexpr uint16_t REG_CH2_OCAL_MSB = 0x14; +static constexpr uint16_t REG_CH2_OCAL_LSB = 0x15; +static constexpr uint16_t REG_CH2_GCAL_MSB = 0x16; +static constexpr uint16_t REG_CH2_GCAL_LSB = 0x17; +static constexpr uint16_t REG_CH3_CFG = 0x18; +static constexpr uint16_t REG_CH3_OCAL_MSB = 0x19; +static constexpr uint16_t REG_CH3_OCAL_LSB = 0x1A; +static constexpr uint16_t REG_CH3_GCAL_MSB = 0x1B; +static constexpr uint16_t REG_CH3_GCAL_LSB = 0x1C; +static constexpr uint16_t REG_CH4_CFG = 0x1D; +static constexpr uint16_t REG_CH4_OCAL_MSB = 0x1E; +static constexpr uint16_t REG_CH4_OCAL_LSB = 0x1F; +static constexpr uint16_t REG_CH4_GCAL_MSB = 0x20; +static constexpr uint16_t REG_CH4_GCAL_LSB = 0x21; +static constexpr uint16_t REG_CH5_CFG = 0x22; +static constexpr uint16_t REG_CH5_OCAL_MSB = 0x23; +static constexpr uint16_t REG_CH5_OCAL_LSB = 0x24; +static constexpr uint16_t REG_CH5_GCAL_MSB = 0x25; +static constexpr uint16_t REG_CH5_GCAL_LSB = 0x26; +static constexpr uint16_t REG_CH6_CFG = 0x27; +static constexpr uint16_t REG_CH6_OCAL_MSB = 0x28; +static constexpr uint16_t REG_CH6_OCAL_LSB = 0x29; +static constexpr uint16_t REG_CH6_GCAL_MSB = 0x2A; +static constexpr uint16_t REG_CH6_GCAL_LSB = 0x2B; +static constexpr uint16_t REG_CH7_CFG = 0x2C; +static constexpr uint16_t REG_CH7_OCAL_MSB = 0x2D; +static constexpr uint16_t REG_CH7_OCAL_LSB = 0x2E; +static constexpr uint16_t REG_CH7_GCAL_MSB = 0x2F; +static constexpr uint16_t REG_CH7_GCAL_LSB = 0x30; +static constexpr uint16_t REGMAP_CRC = 0x3E; +// }; +// Mask READ/WRITE_REG +static constexpr uint16_t MASK_CMD_RW_REG = 0xE000; +static constexpr uint16_t MASK_CMD_RW_REG_ADDRESS = 0x1F80; +static constexpr uint16_t MASK_CMD_RW_REG_COUNT = 0x007F; +static constexpr uint16_t MASK_CMD_RW_REG_RESP = 0xE000; + +// WRITE_REG resp +static constexpr uint16_t WREG_RESP = 0x4000; // mask first with MASK_CMD_RW_REG_RESP +// READ_REG resp +static constexpr uint16_t RREG_RESP = 0xE000; // mask first with MASK_CMD_RW_REG_RESP + +// Mask Register STATUS +static constexpr uint16_t MASK_STATUS_LOCK = 0x8000; +static constexpr uint16_t MASK_STATUS_RESYNC = 0x4000; +static constexpr uint16_t MASK_STATUS_REGMAP = 0x2000; +static constexpr uint16_t MASK_STATUS_CRC_ERR = 0x1000; +static constexpr uint16_t MASK_STATUS_CRC_TYPE = 0x0800; +static constexpr uint16_t MASK_STATUS_RESET = 0x0400; +static constexpr uint16_t MASK_STATUS_WLENGTH = 0x0300; +static constexpr uint16_t MASK_STATUS_DRDY7 = 0x0080; +static constexpr uint16_t MASK_STATUS_DRDY6 = 0x0040; +static constexpr uint16_t MASK_STATUS_DRDY5 = 0x0020; +static constexpr uint16_t MASK_STATUS_DRDY4 = 0x0010; +static constexpr uint16_t MASK_STATUS_DRDY3 = 0x0008; +static constexpr uint16_t MASK_STATUS_DRDY2 = 0x0004; +static constexpr uint16_t MASK_STATUS_DRDY1 = 0x0002; +static constexpr uint16_t MASK_STATUS_DRDY0 = 0x0001; +static constexpr uint16_t MASK_STATUS_DRDY = 0x00FF; + +// Mask Register MODE +static constexpr uint16_t MASK_MODE_REG_CRC_EN = 0x2000; +static constexpr uint16_t MASK_MODE_RX_CRC_EN = 0x1000; +static constexpr uint16_t MASK_MODE_CRC_TYPE = 0x0800; +static constexpr uint16_t MASK_MODE_RESET = 0x0400; +static constexpr uint16_t MASK_MODE_WLENGTH = 0x0300; +static constexpr uint16_t MASK_MODE_TIMEOUT = 0x0010; +static constexpr uint16_t MASK_MODE_DRDY_SEL = 0x000C; +static constexpr uint16_t MASK_MODE_DRDY_HiZ = 0x0002; +static constexpr uint16_t MASK_MODE_DRDY_FMT = 0x0001; + +// Mask Register CLOCK +static constexpr uint16_t MASK_CLOCK_CH7 = 0x8000; +static constexpr uint16_t MASK_CLOCK_CH6 = 0x4000; +static constexpr uint16_t MASK_CLOCK_CH5 = 0x2000; +static constexpr uint16_t MASK_CLOCK_CH4 = 0x1000; +static constexpr uint16_t MASK_CLOCK_CH3 = 0x0800; +static constexpr uint16_t MASK_CLOCK_CH2 = 0x0400; +static constexpr uint16_t MASK_CLOCK_CH1 = 0x0200; +static constexpr uint16_t MASK_CLOCK_CH0 = 0x0100; +static constexpr uint16_t MASK_CLOCK_ALLCH = 0xFF00; +static constexpr uint16_t MASK_CLOCK_ALLCH_OFF = 0x00FF; +static constexpr uint16_t MASK_CLOCK_XTAL_DIS = 0x0080; +static constexpr uint16_t MASK_CLOCK_EXTREF_EN = 0x0040; +static constexpr uint16_t MASK_CLOCK_OSR = 0x001C; +static constexpr uint16_t MASK_CLOCK_PWR = 0x0003; + +static constexpr uint16_t MASK_CLOCK_ALL_CH_ENABLE = 0xFF00; +// Mask Register GAIN +static constexpr uint16_t MASK_GAIN_PGAGAIN7 = 0x7000; +static constexpr uint16_t MASK_GAIN_PGAGAIN6 = 0x0700; +static constexpr uint16_t MASK_GAIN_PGAGAIN5 = 0x0070; +static constexpr uint16_t MASK_GAIN_PGAGAIN4 = 0x0007; +static constexpr uint16_t MASK_GAIN_PGAGAIN3 = 0x7000; +static constexpr uint16_t MASK_GAIN_PGAGAIN2 = 0x0700; +static constexpr uint16_t MASK_GAIN_PGAGAIN1 = 0x0070; +static constexpr uint16_t MASK_GAIN_PGAGAIN0 = 0x0007; + +// Mask Register CFG +static constexpr uint16_t MASK_CFG_GC_DLY = 0x1E00; +static constexpr uint16_t MASK_CFG_GC_EN = 0x0100; +static constexpr uint16_t MASK_CFG_CD_ALLCH = 0x0080; +static constexpr uint16_t MASK_CFG_CD_NUM = 0x0070; +static constexpr uint16_t MASK_CFG_CD_LEN = 0x000E; +static constexpr uint16_t MASK_CFG_CD_EN = 0x0001; + +// Mask Register THRSHLD_MSB - dummy, for completeness +static constexpr uint16_t MASK_THRSHLD_MSB_CD_TH_MSB = 0xFFFF; + +// Mask Register THRSHLD_LSB +static constexpr uint16_t MASK_THRSHLD_LSB_CD_TH_LSB = 0xFF00; +static constexpr uint16_t MASK_THRSHLD_LSB_DCBLOCK = 0x000F; + +// Mask Register CHX_CFG +static constexpr uint16_t MASK_CHX_CFG_PHASE = 0xFFC0; +static constexpr uint16_t MASK_CHX_CFG_DCBLKX_DIS = 0x0004; +static constexpr uint16_t MASK_CHX_CFG_MUX = 0x0003; + +// Mask Register CHX_OCAL_MSB - dummy, for completeness +static constexpr uint16_t MASK_CHX_OCAL_MSB = 0xFFFF; + +// Mask Register CHX_OCAL_LSB +static constexpr uint16_t MASK_CHX_OCAL_LSB = 0xFF00; + +// Mask Register CHX_GCAL_MSB +static constexpr uint16_t MASK_CHX_GCAL_MSB = 0xFFFF; + +// Mask Register CHX_GCAL_LSB +static constexpr uint16_t MASK_CHX_GCAL_LSB = 0xFF00; + +// -------------------------------------------------------------------- + +// Conversion modes +static constexpr uint16_t CONVERSION_MODE_CONT = 0; +static constexpr uint16_t CONVERSION_MODE_SINGLE = 1; + +// Data Format +static constexpr uint16_t DATA_FORMAT_TWO_COMPLEMENT = 0; +static constexpr uint16_t DATA_FORMAT_BINARY = 1; + +// Measure Mode +static constexpr uint8_t MEASURE_UNIPOLAR = 1; +static constexpr uint8_t MEASURE_BIPOLAR = 0; + +// Clock Type +static constexpr uint8_t CLOCK_EXTERNAL = 1; +static constexpr uint8_t CLOCK_INTERNAL = 0; + +// PGA Gain +// static constexpr uint16_t PGA_GAIN_1 = 0; +// static constexpr uint16_t PGA_GAIN_2 = 1; +// static constexpr uint16_t PGA_GAIN_4 = 2; +// static constexpr uint16_t PGA_GAIN_8 = 3; +// static constexpr uint16_t PGA_GAIN_16 = 4; +// static constexpr uint16_t PGA_GAIN_32 = 5; +// static constexpr uint16_t PGA_GAIN_64 = 6; +// static constexpr uint16_t PGA_GAIN_128 = 7; + +// Input Filter +static constexpr uint16_t FILTER_SYNC = 0; +static constexpr uint16_t FILTER_FIR = 2; +static constexpr uint16_t FILTER_FIR_IIR = 3; + +// Data Mode +static constexpr uint8_t DATA_MODE_24BITS = 0; +static constexpr uint8_t DATA_MODE_32BITS = 1; + +// Data Rate +static constexpr uint8_t DATA_RATE_0 = 0; +static constexpr uint8_t DATA_RATE_1 = 1; +static constexpr uint8_t DATA_RATE_2 = 2; +static constexpr uint8_t DATA_RATE_3 = 3; +static constexpr uint8_t DATA_RATE_4 = 4; +static constexpr uint8_t DATA_RATE_5 = 5; +static constexpr uint8_t DATA_RATE_6 = 6; +static constexpr uint8_t DATA_RATE_7 = 7; +static constexpr uint8_t DATA_RATE_8 = 8; +static constexpr uint8_t DATA_RATE_9 = 9; +static constexpr uint8_t DATA_RATE_10 = 10; +static constexpr uint8_t DATA_RATE_11 = 11; +static constexpr uint8_t DATA_RATE_12 = 12; +static constexpr uint8_t DATA_RATE_13 = 13; +static constexpr uint8_t DATA_RATE_14 = 14; +static constexpr uint8_t DATA_RATE_15 = 15; +// Sync Mpdes +static constexpr uint16_t SYNC_CONTINUOUS = 1; +static constexpr uint16_t SYNC_PULSE = 0; + +// DIO Config Mode +static constexpr uint8_t DIO_OUTPUT = 1; +static constexpr uint8_t DIO_INPUT = 0; + +static constexpr uint8_t SPI_MASTER_DUMMY = 0xFF; +static constexpr uint16_t SPI_MASTER_DUMMY16 = 0xFFFF; +static constexpr uint32_t SPI_MASTER_DUMMY32 = 0xFFFFFFFF; +// end of from datasheet + +static constexpr size_t BYTE_BITLENGTH = 8; // uint8/int8 +static constexpr size_t WORD_BITLENGTH = 16; // uint16/int16 +static constexpr size_t DWORD_BITLENGTH = 32; // uint32/int32/float +static constexpr size_t QWORD_BITLENGTH = 64; // uint64/int64/float64 + +} // namespace ads131m08 +} // namespace esphome diff --git a/ads131m08/git_errors.txt b/ads131m08/git_errors.txt new file mode 100644 index 0000000..e2db25e --- /dev/null +++ b/ads131m08/git_errors.txt @@ -0,0 +1,104 @@ +ruff (legacy alias)......................................................Failed +- hook id: ruff +- exit code: 1 +- files were modified by this hook + +F401 `esphome.const.CONF_ID` imported but unused; consider removing, adding to `__all__`, or using a redundant alias + --> esphome/components/ads131m08/sensor_rms/__init__.py:6:5 + | +4 | from esphome.const import ( +5 | CONF_CHANNEL, +6 | CONF_ID, + | ^^^^^^^ +7 | CONF_NAME, +8 | DEVICE_CLASS_VOLTAGE, + | +help: Use an explicit re-export: `CONF_ID as CONF_ID` + +E402 Module level import not at top of file + --> esphome/components/ads131m08/sensor_rms/__init__.py:20:1 + | +18 | MAX_CHANNELS = 12 +19 | +20 | from .. import CONF_ADS131M08_ID, ads131m08_ns + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +21 | +22 | AUTO_LOAD = [ + | + +Found 6 errors (4 fixed, 2 remaining). + +ruff format..............................................................Failed +- hook id: ruff-format +- files were modified by this hook + +3 files reformatted + +flake8...................................................................Failed +- hook id: flake8 +- exit code: 1 + +esphome/components/ads131m08/sensor_rms/__init__.py:4:1: F401 'esphome.const.CONF_ID' imported but unused +esphome/components/ads131m08/sensor_rms/__init__.py:20:1: E402 module level import not at top of file + +don't commit to branch...................................................Failed +- hook id: no-commit-to-branch +- exit code: 1 +fix end of files.........................................................Failed +- hook id: end-of-file-fixer +- exit code: 1 +- files were modified by this hook + +Fixing esphome/components/ads131m08/README.md +Fixing esphome/components/ads131m08/ads131m08.h +Fixing esphome/components/ads131m08/uint_str.h + +trim trailing whitespace.................................................Failed +- hook id: trailing-whitespace +- exit code: 1 +- files were modified by this hook + +Fixing esphome/components/ads131m08/ads131m08_defs.h +Fixing esphome/components/ads131m08/README.md +Fixing esphome/components/ads131m08/sensor/ads131m08_sensor.cpp +Fixing esphome/components/ads131m08/ads131m08.cpp +Fixing esphome/components/ads131m08/ads131m08.h +Fixing esphome/components/ads131m08/sensor/ads131m08_sensor.h +Fixing esphome/components/ads131m08/uint_str.h + +pyupgrade................................................................Passed +yamllint.............................................(no files to check)Skipped +clang-format.............................................................Failed +- hook id: clang-format +- files were modified by this hook +pylint...................................................................Failed +- hook id: pylint +- exit code: 16 + +************* Module esphome.components.ads131m08.sensor_rms +esphome/components/ads131m08/sensor_rms/__init__.py:20:0: C0413: Import "from .. import CONF_ADS131M08_ID, ads131m08_ns" should be placed at the top of the module (wrong-import-position) + +Update clang-tidy hash...............................(no files to check)Skipped +ci-custom................................................................Failed +- hook id: ci-custom +- exit code: 2 + +### File esphome/components/ads131m08/__init__.py + +esphome/components/ads131m08/__init__.py:13:2: lint: Constant CONF_OSR does not match value oversampling_ratio! Please make sure the constant's name matches its value! + + +### File esphome/components/ads131m08/sensor/__init__.py + +esphome/components/ads131m08/sensor/__init__.py:14:2: lint: Constant CONF_OFFSET_CAL does not match value offset_calibration! Please make sure the constant's name matches its value! + +esphome/components/ads131m08/sensor/__init__.py:16:2: lint: Constant CONF_GAIN_CAL does not match value gain_calibration! Please make sure the constant's name matches its value! + +esphome/components/ads131m08/sensor/__init__.py:17:2: lint: Constant CONF_PHASE_CAL does not match value phase_calibration! Please make sure the constant's name matches its value! + +esphome/components/ads131m08/sensor/__init__.py:18:2: lint: Constant CONF_MUX_INP does not match value input_select! Please make sure the constant's name matches its value! + + +### File esphome/components/ads131m08/sensor_rms/__init__.py + +esphome/components/ads131m08/sensor_rms/__init__.py:16:2: lint: Constant CONF_CALC_RMS does not match value read_rms! Please make sure the constant's name matches its value! diff --git a/ads131m08/sensor/__init__.py b/ads131m08/sensor/__init__.py new file mode 100644 index 0000000..e68683f --- /dev/null +++ b/ads131m08/sensor/__init__.py @@ -0,0 +1,91 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, voltage_sampler +from esphome.const import CONF_ID, CONF_GAIN, CONF_CHANNEL +from .. import CONF_ADS131M08_ID, ADS131M08Hub, ads131m08_ns +from ..sensor_rms import CONFIG_SCHEMA_RMS_SENSORS, to_code_ac, to_code_dc + +AUTO_LOAD = [ + "sensor", "voltage_sampler", +] + +CONF_OFFSET_CALIBRATION = "offset_calibration" +CONF_GAIN_CALIBRATION = "gain_calibration" +CONF_PHASE_CALIBRATION = "phase_calibration" +CONF_INPUT_SELECT = "input_select" +ICON_CURRENT_DC = "mdi:current-dc" +MAX_CHANNELS = 12 + +DEPENDENCIES = ["ads131m08"] + +GAIN = ads131m08_ns.enum("ADC_PGA_GAIN") +ALLOWED_GAINS = { + "1": GAIN.PGA_1, + "2": GAIN.PGA_2, + "4": GAIN.PGA_4, + "8": GAIN.PGA_8, + "16": GAIN.PGA_16, + "32": GAIN.PGA_32, + "64": GAIN.PGA_64, + "128": GAIN.PGA_128, + 1: GAIN.PGA_1, + 2: GAIN.PGA_2, + 4: GAIN.PGA_4, + 8: GAIN.PGA_8, + 16: GAIN.PGA_16, + 32: GAIN.PGA_32, + 64: GAIN.PGA_64, + 128: GAIN.PGA_128, +} + +ADC_INPUT = ads131m08_ns.enum("ADC_INPUT_CHANNEL_MUX") +ALLOWED_MUX_INP = { + "normal": ADC_INPUT.ICM_AIN0P_AIN0N, + "shorted" : ADC_INPUT.ICM_INPUT_SHORTED, + "positive_dc" : ADC_INPUT.ICM_POSITIVE_DC_TEST_SIGNAL, + "negative_dc" : ADC_INPUT.ICM_NEGATIVE_DC_TEST_SIGNAL, +} + +Channel = ads131m08_ns.class_( + "Channel", sensor.Sensor, cg.Component, voltage_sampler.VoltageSampler +) + +CONFIG_SCHEMA = CONFIG_SCHEMA_RMS_SENSORS.extend( + { + cv.GenerateID(CONF_ADS131M08_ID): cv.use_id(ADS131M08Hub), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=MAX_CHANNELS), + cv.Optional(CONF_GAIN, default=1): cv.enum(ALLOWED_GAINS, int=True), + cv.Optional(CONF_OFFSET_CALIBRATION, default=0): cv.int_range(min=-8388608, max=8388607), # should use volts, but need to figure out conversion function first + cv.Optional(CONF_GAIN_CALIBRATION, default=1): cv.float_range(min=0, max=2), + cv.Optional(CONF_PHASE_CALIBRATION, default=0): cv.int_range(min=-512, max=511), # should use degrees, but need to figure out conversion function first + cv.Optional(CONF_INPUT_SELECT, default='normal'): cv.enum(ALLOWED_MUX_INP, int=False), + } +).extend( + { + cv.GenerateID(): cv.declare_id(Channel) + } +).extend(cv.COMPONENT_SCHEMA) +# we are using 3 sensors: +# 1. channel_sensor: this represents 1 of the 8 ads131m08 channels and is used to program the adc channel gain, offset calibration, etc. This sensor publishes instantaneous sampled value +# 2. dc_sensor: to publish averaged dc value +# 3. ac_sensor: to publish rms ac value +async def to_code(config): + channel_sensor = cg.new_Pvariable(config[CONF_ID]) + await cg.register_parented(channel_sensor, config[CONF_ADS131M08_ID]) + channel = config[CONF_CHANNEL] + cg.add(channel_sensor.set_channel_number(channel)) + gain = config[CONF_GAIN] + cg.add(channel_sensor.set_gain(gain)) + mux_inp = config[CONF_INPUT_SELECT] + cg.add(channel_sensor.set_mux_input(mux_inp)) + gain_cal = config[CONF_GAIN_CALIBRATION] + cg.add(channel_sensor.set_gain_calibration(gain_cal)) + offset_cal = config[CONF_OFFSET_CALIBRATION] + cg.add(channel_sensor.set_offset_calibration(offset_cal)) + phase_cal = config[CONF_PHASE_CALIBRATION] + cg.add(channel_sensor.set_phase_calibration(phase_cal)) + await cg.register_component(channel_sensor, config) + if dc_sensor := await to_code_dc(config): + await cg.register_parented(dc_sensor, channel_sensor) + if ac_sensor := await to_code_ac(config): + await cg.register_parented(ac_sensor, channel_sensor) diff --git a/ads131m08/sensor/ads131m08_sensor.cpp b/ads131m08/sensor/ads131m08_sensor.cpp new file mode 100644 index 0000000..f585e43 --- /dev/null +++ b/ads131m08/sensor/ads131m08_sensor.cpp @@ -0,0 +1,90 @@ +#include "ads131m08_sensor.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace ads131m08 { + +static const char *const TAG = "adsm131m08.sensor"; + +void Channel::loop() +{ + if(this->first_reading_) { + if(this->parent_ != nullptr) { + this->parent_->set_channel_gain(this->channel_, this->gain_); + this->parent_->set_channel_input_selection(this->channel_, this->input_); + this->parent_->set_channel_phase_calibration(this->channel_, this->normalised_phase_calibration()); + this->parent_->set_channel_offset_calibration(this->channel_, this->normalised_offset_calibration()); + this->parent_->set_channel_gain_calibration(this->channel_, this->normalised_gain_calibration()); + this->parent_->set_dcblock_filter_disable(this->channel_, true); + this->parent_->set_channel_enable(this->channel_, true); + this->first_reading_ = false; + } + } +} + +float Channel::sample() +{ + return (this->parent_ == nullptr) ? NAN : this->parent_->get_sampled_value(this->channel_); +} + +// converts gain of 0.0 - 2.0 to the corresponding values that ADS131M08 expects +uint32_t Channel::normalised_gain_calibration() const +{ + uint32_t result = (uint32_t)lroundf(gain_cal_ * 8388608.0f); + return result & 0xFFFFFF; +} + +uint32_t Channel::normalised_offset_calibration() const +{ + uint32_t result = (uint32_t)offset_cal_; + return result & 0xFFFFFF; +} + +int16_t Channel::normalised_phase_calibration() const +{ + int16_t result = (int16_t)phase_cal_; + return result; +} + +void Channel::dump_config() { + ESP_LOGCONFIG(TAG, "Channel %" PRIu8 ":", this->channel_); + ESP_LOGCONFIG(TAG, " Gain: %" PRIu8, 1 << this->gain_); + ESP_LOGCONFIG(TAG, " Gain calibration: %f", this->gain_cal_); + ESP_LOGCONFIG(TAG, " Offset calibration: %" PRIu32, this->offset_cal_); + ESP_LOGCONFIG(TAG, " Phase calibration: %" PRId32, this->phase_cal_); + ESP_LOGCONFIG(TAG, " Input select: %s", (this->input_ == ICM_AIN0P_AIN0N) ? "normal" \ + : (this->input_ == ICM_INPUT_SHORTED) ? "shorted" \ + : (this->input_ == ICM_POSITIVE_DC_TEST_SIGNAL) ? "positive_dc" \ + : (this->input_ == ICM_NEGATIVE_DC_TEST_SIGNAL) ? "negative_dc" \ + : "invalid"); +} + + +void RMS_Sensor::loop() +{ + if(!this->set_calc_rms_) { + if(this->parent_ != nullptr) { + if(this->calc_rms_) { + this->set_calc_rms_ = this->parent_->set_calc_rms(true); + } + else { + this->set_calc_rms_ = true; + } + } + } + else { + float value = this->parent_->get_average(this->calc_rms_); + publish_state(value); + } +} + +void RMS_Sensor::dump_config() +{ + LOG_SENSOR(" ", "Sensor", this); + ESP_LOGCONFIG(TAG, " Calculate rms: %s", this->calc_rms_ ? "YES" : "NO"); +} + +} // namespace ads131m08_acdc + +} // namespace esphome diff --git a/ads131m08/sensor/ads131m08_sensor.h b/ads131m08/sensor/ads131m08_sensor.h new file mode 100644 index 0000000..ac18aff --- /dev/null +++ b/ads131m08/sensor/ads131m08_sensor.h @@ -0,0 +1,63 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" + +#include "../ads131m08.h" + + +namespace esphome { +namespace ads131m08 { + +class Channel : public sensor::Sensor, + public Component, + public voltage_sampler::VoltageSampler, + public Parented { + public: + void loop() override; + float sample() override; + void set_gain(uint8_t value) { this->gain_ = value; } + void dump_config() override; + void set_channel_number(uint8_t channel) { this->channel_= channel; } + void set_gain_calibration(float value) { this->gain_cal_ = value; } + void set_offset_calibration(int value) { this->offset_cal_ = value; } + void set_phase_calibration(int value) { this->phase_cal_ = value; } + bool set_calc_rms(bool enable) { this->calc_rms_ = enable; return this->parent_ != nullptr ? this->parent_->set_measure_rms(this->channel_, enable) : false; } + float get_average(bool read_ac) { return this->parent_->get_average(this->channel_, read_ac); } + void set_mux_input(ADC_INPUT_CHANNEL_MUX value) { this->input_ = value; } + uint32_t normalised_gain_calibration() const; + uint32_t normalised_offset_calibration() const; + int16_t normalised_phase_calibration() const; + + protected: + uint8_t channel_; + int phase_cal_; + int offset_cal_; + float gain_cal_; + bool calc_rms_; + uint8_t gain_; + ADC_INPUT_CHANNEL_MUX input_; + bool first_reading_{true}; + +}; + +class RMS_Sensor : public sensor::Sensor, + public Component, + public Parented { + public: + void loop() override; + void dump_config() override; + void set_calc_rms(bool enable) { this->calc_rms_ = enable; } + + protected: + bool calc_rms_{false}; + bool set_calc_rms_{false}; +}; + + +} // namespace ads131m08 + +} // namespace esphome diff --git a/ads131m08/sensor_rms/__init__.py b/ads131m08/sensor_rms/__init__.py new file mode 100644 index 0000000..009a072 --- /dev/null +++ b/ads131m08/sensor_rms/__init__.py @@ -0,0 +1,75 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + ICON_CURRENT_AC, + UNIT_VOLT, + CONF_CHANNEL, + CONF_NAME, +) +from .. import CONF_ADS131M08_ID, ads131m08_ns + +CONF_AC = "ac_1" +CONF_DC = "dc" +ICON_CURRENT_DC = "mdi:current-dc" +MAX_CHANNELS = 12 +AUTO_LOAD = ["sensor",] + +DEPENDENCIES = ["ads131m08"] + +ads131m08_acdc_ns = cg.esphome_ns.namespace("ads131m08_acdc") + +RMS_SENSOR = ads131m08_ns.class_( + "RMS_Sensor", sensor.Sensor, cg.Component +) + +CONFIG_SCHEMA_RMS_SENSORS = cv.Schema( + { + cv.Required(CONF_DC): sensor.sensor_schema( + RMS_SENSOR, + accuracy_decimals=6, + icon=ICON_CURRENT_DC, + unit_of_measurement=UNIT_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Required(CONF_NAME): cv.string, + } + ), + cv.Optional(CONF_AC): sensor.sensor_schema( + RMS_SENSOR, + accuracy_decimals=6, + icon=ICON_CURRENT_AC, + unit_of_measurement=UNIT_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Required(CONF_NAME): cv.string, + } + ), + } +). extend (cv.COMPONENT_SCHEMA) +async def to_code_ac(config): + hub = await cg.get_variable(config[CONF_ADS131M08_ID]) + channel = config[CONF_CHANNEL] + if ac_config := config.get(CONF_AC): + sens_ac = await sensor.new_sensor(ac_config) + cg.add(sens_ac.set_calc_rms(True)) + cg.add(hub.register_sensor_ac(channel, sens_ac)) + await cg.register_component(sens_ac, config[CONF_AC]) + return sens_ac + return None + +async def to_code_dc(config): + hub = await cg.get_variable(config[CONF_ADS131M08_ID]) + channel = config[CONF_CHANNEL] + dc_config = config.get(CONF_DC) + sens_dc = await sensor.new_sensor(dc_config) + cg.add(sens_dc, config.get(CONF_DC)) + cg.add(hub.register_sensor_dc(channel, sens_dc)) + await cg.register_component(sens_dc, config[CONF_DC]) + return sens_dc \ No newline at end of file diff --git a/ads131m08/uint_str.h b/ads131m08/uint_str.h new file mode 100644 index 0000000..dfc5081 --- /dev/null +++ b/ads131m08/uint_str.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +namespace esphome { +namespace ads131m08 { + +template struct ci_char_traits : public std::char_traits { + static bool eq(chartype c1, chartype c2) { return c1 == c2; } + static bool ne(chartype c1, chartype c2) { return c1 != c2; } + static bool lt(chartype c1, chartype c2) { return c1 < c2; } + static int compare(const chartype *a, const chartype *b, size_t _Count) { + for (; 0 < _Count; --_Count, ++a, ++b) { + if (*a == *b) + continue; + return (*a < *b ? -1 : +1); + } + return (0); + } +}; + +template class uint_str { + public: + typedef std::basic_string, std::allocator > Ty_string; +}; + +} // namespace ads131m08 +} // namespace esphome diff --git a/canbus/__init__.py b/canbus/__init__.py new file mode 100644 index 0000000..7b51c2c --- /dev/null +++ b/canbus/__init__.py @@ -0,0 +1,183 @@ +import re + +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_DATA, CONF_ID, CONF_TRIGGER_ID +from esphome.core import CORE, ID + +CODEOWNERS = ["@mvturnho", "@danielschramm"] +IS_PLATFORM_COMPONENT = True + +CONF_CAN_ID = "can_id" +CONF_CAN_ID_MASK = "can_id_mask" +CONF_USE_EXTENDED_ID = "use_extended_id" +CONF_REMOTE_TRANSMISSION_REQUEST = "remote_transmission_request" +CONF_CANBUS_ID = "canbus_id" +CONF_BIT_RATE = "bit_rate" +CONF_ON_FRAME = "on_frame" + + +def validate_id(config): + if CONF_CAN_ID in config: + can_id = config[CONF_CAN_ID] + id_ext = config[CONF_USE_EXTENDED_ID] + if not id_ext and can_id > 0x7FF: + raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)") + return config + + +def validate_raw_data(value): + if isinstance(value, str): + return value.encode("utf-8") + if isinstance(value, list): + return cv.Schema([cv.hex_uint8_t])(value) + raise cv.Invalid( + "data must either be a string wrapped in quotes or a list of bytes" + ) + + +canbus_ns = cg.esphome_ns.namespace("canbus") +CanbusComponent = canbus_ns.class_("CanbusComponent", cg.Component) +CanbusTrigger = canbus_ns.class_( + "CanbusTrigger", + automation.Trigger.template(cg.std_vector.template(cg.uint8), cg.uint32), + cg.Component, +) +CanSpeed = canbus_ns.enum("CAN_SPEED") + +CAN_SPEEDS = { + "1KBPS": CanSpeed.CAN_1KBPS, + "5KBPS": CanSpeed.CAN_5KBPS, + "10KBPS": CanSpeed.CAN_10KBPS, + "12K5BPS": CanSpeed.CAN_12K5BPS, + "16KBPS": CanSpeed.CAN_16KBPS, + "20KBPS": CanSpeed.CAN_20KBPS, + "25KBPS": CanSpeed.CAN_25KBPS, + "31K25BPS": CanSpeed.CAN_31K25BPS, + "33KBPS": CanSpeed.CAN_33KBPS, + "40KBPS": CanSpeed.CAN_40KBPS, + "50KBPS": CanSpeed.CAN_50KBPS, + "80KBPS": CanSpeed.CAN_80KBPS, + "83K3BPS": CanSpeed.CAN_83K3BPS, + "95KBPS": CanSpeed.CAN_95KBPS, + "100KBPS": CanSpeed.CAN_100KBPS, + "125KBPS": CanSpeed.CAN_125KBPS, + "200KBPS": CanSpeed.CAN_200KBPS, + "250KBPS": CanSpeed.CAN_250KBPS, + "500KBPS": CanSpeed.CAN_500KBPS, + "800KBPS": CanSpeed.CAN_800KBPS, + "1000KBPS": CanSpeed.CAN_1000KBPS, +} + + +def get_rate(value): + match = re.match(r"(\d+)(?:K(\d+)?)?BPS", value, re.IGNORECASE) + if not match: + raise ValueError(f"Invalid rate format: {value}") + fraction = match.group(2) or "0" + return int((float(match.group(1)) + float(f"0.{fraction}")) * 1000) + + +CANBUS_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(CanbusComponent), + cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF), + cv.Optional(CONF_BIT_RATE, default="125KBPS"): cv.enum(CAN_SPEEDS, upper=True), + cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, + cv.Optional(CONF_ON_FRAME): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CanbusTrigger), + cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF), + cv.Optional(CONF_CAN_ID_MASK, default=0x1FFFFFFF): cv.int_range( + min=0, max=0x1FFFFFFF + ), + cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, + cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST): cv.boolean, + }, + validate_id, + ), + }, +).extend(cv.COMPONENT_SCHEMA) + +CANBUS_SCHEMA.add_extra(validate_id) + + +async def setup_canbus_core_(var, config): + await cg.register_component(var, config) + cg.add(var.set_can_id([config[CONF_CAN_ID]])) + cg.add(var.set_use_extended_id([config[CONF_USE_EXTENDED_ID]])) + cg.add(var.set_bitrate(CAN_SPEEDS[config[CONF_BIT_RATE]])) + + for conf in config.get(CONF_ON_FRAME, []): + can_id = conf[CONF_CAN_ID] + can_id_mask = conf[CONF_CAN_ID_MASK] + ext_id = conf[CONF_USE_EXTENDED_ID] + trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], var, can_id, can_id_mask, ext_id + ) + if CONF_REMOTE_TRANSMISSION_REQUEST in conf: + cg.add( + trigger.set_remote_transmission_request( + conf[CONF_REMOTE_TRANSMISSION_REQUEST] + ) + ) + await cg.register_component(trigger, conf) + await automation.build_automation( + trigger, + [ + (cg.std_vector.template(cg.uint8), "x"), + (cg.uint32, "can_id"), + (cg.bool_, "remote_transmission_request"), + ], + conf, + ) + + +async def register_canbus(var, config): + if not CORE.has_id(config[CONF_ID]): + var = cg.new_Pvariable(config[CONF_ID], var) + await setup_canbus_core_(var, config) + + +# Actions +@automation.register_action( + "canbus.send", + canbus_ns.class_("CanbusSendAction", automation.Action), + cv.maybe_simple_value( + { + cv.GenerateID(CONF_CANBUS_ID): cv.use_id(CanbusComponent), + cv.Optional(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF), + cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, + cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST, default=False): cv.boolean, + cv.Required(CONF_DATA): cv.templatable(validate_raw_data), + }, + validate_id, + key=CONF_DATA, + ), +) +async def canbus_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_CANBUS_ID]) + + if can_id := config.get(CONF_CAN_ID): + can_id = await cg.templatable(can_id, args, cg.uint32) + cg.add(var.set_can_id(can_id)) + cg.add(var.set_use_extended_id(config[CONF_USE_EXTENDED_ID])) + + cg.add( + var.set_remote_transmission_request(config[CONF_REMOTE_TRANSMISSION_REQUEST]) + ) + + data = config[CONF_DATA] + if cg.is_template(data): + templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) + cg.add(var.set_data_template(templ)) + else: + if isinstance(data, bytes): + data = [int(x) for x in data] + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) + return var diff --git a/canbus/canbus.cpp b/canbus/canbus.cpp new file mode 100644 index 0000000..e208b0f --- /dev/null +++ b/canbus/canbus.cpp @@ -0,0 +1,106 @@ +#include "canbus.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace canbus { + +static const char *const TAG = "canbus"; + +void Canbus::setup() { + if (!this->setup_internal()) { + ESP_LOGE(TAG, "setup error!"); + this->mark_failed(); + } +} + +void Canbus::dump_config() { + if (this->use_extended_id_) { + ESP_LOGCONFIG(TAG, "config extended id=0x%08" PRIx32, this->can_id_); + } else { + ESP_LOGCONFIG(TAG, "config standard id=0x%03" PRIx32, this->can_id_); + } +} + +canbus::Error Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request, + const std::vector &data) { + struct CanFrame can_message; + + uint8_t size = static_cast(data.size()); + if (use_extended_id) { + ESP_LOGD(TAG, "send extended id=0x%08" PRIx32 " rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), + size); + } else { + ESP_LOGD(TAG, "send standard id=0x%03" PRIx32 " rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), + size); + } + if (size > CAN_MAX_DATA_LENGTH) + size = CAN_MAX_DATA_LENGTH; + can_message.can_data_length_code = size; + can_message.can_id = can_id; + can_message.use_extended_id = use_extended_id; + can_message.remote_transmission_request = remote_transmission_request; + + for (int i = 0; i < size; i++) { + can_message.data[i] = data[i]; + ESP_LOGVV(TAG, " data[%d]=%02x", i, can_message.data[i]); + } + + canbus::Error error = this->send_message(&can_message); + if (error != canbus::ERROR_OK) { + if (use_extended_id) { + ESP_LOGW(TAG, "send to extended id=0x%08" PRIx32 " failed with error %d!", can_id, error); + } else { + ESP_LOGW(TAG, "send to standard id=0x%03" PRIx32 " failed with error %d!", can_id, error); + } + } + return error; +} + +void Canbus::add_trigger(CanbusTrigger *trigger) { + if (trigger->use_extended_id_) { + ESP_LOGVV(TAG, "add trigger for extended canid=0x%08" PRIx32, trigger->can_id_); + } else { + ESP_LOGVV(TAG, "add trigger for std canid=0x%03" PRIx32, trigger->can_id_); + } + this->triggers_.push_back(trigger); +}; + +void Canbus::loop() { + struct CanFrame can_message; + // read all messages until queue is empty + int message_counter = 0; + while (this->read_message(&can_message) == canbus::ERROR_OK) { + message_counter++; + if (can_message.use_extended_id) { + ESP_LOGD(TAG, "received can message (#%d) extended can_id=0x%" PRIx32 " size=%d", message_counter, + can_message.can_id, can_message.can_data_length_code); + } else { + ESP_LOGD(TAG, "received can message (#%d) std can_id=0x%" PRIx32 " size=%d", message_counter, can_message.can_id, + can_message.can_data_length_code); + } + + std::vector data; + + // show data received + for (int i = 0; i < can_message.can_data_length_code; i++) { + ESP_LOGV(TAG, " can_message.data[%d]=%02x", i, can_message.data[i]); + data.push_back(can_message.data[i]); + } + + this->callback_manager_(can_message.can_id, can_message.use_extended_id, can_message.remote_transmission_request, + data); + + // fire all triggers + for (auto *trigger : this->triggers_) { + if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) && + (trigger->use_extended_id_ == can_message.use_extended_id) && + (!trigger->remote_transmission_request_.has_value() || + trigger->remote_transmission_request_.value() == can_message.remote_transmission_request)) { + trigger->trigger(data, can_message.can_id, can_message.remote_transmission_request); + } + } + } +} + +} // namespace canbus +} // namespace esphome diff --git a/canbus/canbus.h b/canbus/canbus.h new file mode 100644 index 0000000..f7b8411 --- /dev/null +++ b/canbus/canbus.h @@ -0,0 +1,184 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/optional.h" + +#include +#include + +namespace esphome { +namespace canbus { + +enum Error : uint8_t { + ERROR_OK = 0, + ERROR_FAIL = 1, + ERROR_ALLTXBUSY = 2, + ERROR_FAILINIT = 3, + ERROR_FAILTX = 4, + ERROR_NOMSG = 5 +}; + +enum CanSpeed : uint8_t { + CAN_1KBPS, + CAN_5KBPS, + CAN_10KBPS, + CAN_12K5BPS, + CAN_16KBPS, + CAN_20KBPS, + CAN_25KBPS, + CAN_31K25BPS, + CAN_33KBPS, + CAN_40KBPS, + CAN_50KBPS, + CAN_80KBPS, + CAN_83K3BPS, + CAN_95KBPS, + CAN_100KBPS, + CAN_125KBPS, + CAN_200KBPS, + CAN_250KBPS, + CAN_500KBPS, + CAN_800KBPS, + CAN_1000KBPS +}; + +class CanbusTrigger; +template class CanbusSendAction; + +/* CAN payload length definitions according to ISO 11898-1 */ +static const uint8_t CAN_MAX_DATA_LENGTH = 8; + +/* +Can Frame describes a normative CAN Frame +The RTR = Remote Transmission Request is implemented in every CAN controller but rarely used +So currently the flag is passed to and from the hardware but currently ignored to the user application. +*/ +struct CanFrame { + bool use_extended_id = false; + bool remote_transmission_request = false; + uint32_t can_id; /* 29 or 11 bit CAN_ID */ + uint8_t can_data_length_code; /* frame payload length in byte (0 .. CAN_MAX_DATA_LENGTH) */ + uint8_t data[CAN_MAX_DATA_LENGTH] __attribute__((aligned(8))); +}; + +class Canbus : public Component { + public: + Canbus(){}; + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void loop() override; + + canbus::Error send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request, + const std::vector &data); + canbus::Error send_data(uint32_t can_id, bool use_extended_id, const std::vector &data) { + // for backwards compatibility only + return this->send_data(can_id, use_extended_id, false, data); + } + void set_can_id(uint32_t can_id) { this->can_id_ = can_id; } + void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; } + void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; } + + void add_trigger(CanbusTrigger *trigger); + /** + * Add a callback to be called when a CAN message is received. All received messages + * are passed to the callback without filtering. + * + * The callback function receives: + * - can_id of the received data + * - extended_id True if the can_id is an extended id + * - rtr If this is a remote transmission request + * - data The message data + */ + void add_callback( + std::function &data)> callback) { + this->callback_manager_.add(std::move(callback)); + } + + protected: + template friend class CanbusSendAction; + std::vector triggers_{}; + uint32_t can_id_; + bool use_extended_id_; + CanSpeed bit_rate_; + CallbackManager &data)> + callback_manager_{}; + + virtual bool setup_internal() = 0; + virtual Error send_message(struct CanFrame *frame) = 0; + virtual Error read_message(struct CanFrame *frame) = 0; +}; + +template class CanbusSendAction : public Action, public Parented { + public: + void set_data_template(std::vector (*func)(Ts...)) { + // Stateless lambdas (generated by ESPHome) implicitly convert to function pointers + this->data_.func = func; + this->len_ = -1; // Sentinel value indicates template mode + } + + // Store pointer to static data in flash (no RAM copy) + void set_data_static(const uint8_t *data, size_t len) { + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode + } + + void set_can_id(uint32_t can_id) { this->can_id_ = can_id; } + + void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; } + + void set_remote_transmission_request(bool remote_transmission_request) { + this->remote_transmission_request_ = remote_transmission_request; + } + + void play(const Ts &...x) override { + auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_; + auto use_extended_id = + this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_; + std::vector data; + if (this->len_ >= 0) { + // Static mode: copy from flash to vector + data.assign(this->data_.data, this->data_.data + this->len_); + } else { + // Template mode: call function + data = this->data_.func(x...); + } + this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data); + } + + protected: + optional can_id_{}; + optional use_extended_id_{}; + bool remote_transmission_request_{false}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Data { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } data_; +}; + +class CanbusTrigger : public Trigger, uint32_t, bool>, public Component { + friend class Canbus; + + public: + explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const std::uint32_t can_id_mask, + const bool use_extended_id) + : parent_(parent), can_id_(can_id), can_id_mask_(can_id_mask), use_extended_id_(use_extended_id){}; + + void set_remote_transmission_request(bool remote_transmission_request) { + this->remote_transmission_request_ = remote_transmission_request; + } + + void setup() override { this->parent_->add_trigger(this); } + + protected: + Canbus *parent_; + uint32_t can_id_; + uint32_t can_id_mask_; + bool use_extended_id_; + optional remote_transmission_request_{}; +}; + +} // namespace canbus +} // namespace esphome diff --git a/ds3231/__init__.py b/ds3231/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ds3231/ds3231.cpp b/ds3231/ds3231.cpp new file mode 100644 index 0000000..6bb5185 --- /dev/null +++ b/ds3231/ds3231.cpp @@ -0,0 +1,112 @@ +#include "ds3231.h" +#include "esphome/core/log.h" + +// Datasheet: +// - https://datasheets.maximintegrated.com/en/ds/DS3231.pdf + +namespace esphome { +namespace ds3231 { + +static const char *const TAG = "ds3231"; + +void DS3231Component::setup() { + if (!this->read_rtc_()) { + this->mark_failed(); + } +} + +void DS3231Component::update() { this->read_time(); } + +void DS3231Component::dump_config() { + ESP_LOGCONFIG(TAG, "DS3231:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +// ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); + if (!this->read_bytes(0, this->ds3231_.raw, sizeof(this->ds3231_.raw))) { + ESP_LOGE(TAG, "Can't read I2C data."); + } +} + +float DS3231Component::get_setup_priority() const { return setup_priority::DATA; } + +void DS3231Component::read_time() { + if (!this->read_rtc_()) { + return; + } + if (ds3231_.reg.ch) { + ESP_LOGW(TAG, "RTC halted, not syncing to system clock."); + return; + } + ESPTime rtc_time{ + .second = uint8_t(ds3231_.reg.second + 10 * ds3231_.reg.second_10), + .minute = uint8_t(ds3231_.reg.minute + 10u * ds3231_.reg.minute_10), + .hour = uint8_t(ds3231_.reg.hour + 10u * ds3231_.reg.hour_10), + .day_of_week = uint8_t(ds3231_.reg.weekday), + .day_of_month = uint8_t(ds3231_.reg.day + 10u * ds3231_.reg.day_10), + .day_of_year = 1, // ignored by recalc_timestamp_utc(false) + .month = uint8_t(ds3231_.reg.month + 10u * ds3231_.reg.month_10), + .year = uint16_t(ds3231_.reg.year + 10u * ds3231_.reg.year_10 + 2000), + .is_dst = false, // not used + .timestamp = 0 // overwritten by recalc_timestamp_utc(false) + + }; + rtc_time.recalc_timestamp_utc(false); + if (!rtc_time.is_valid()) { + ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); + return; + } + time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp); +} + +void DS3231Component::write_time() { + auto now = time::RealTimeClock::utcnow(); + if (!now.is_valid()) { + ESP_LOGE(TAG, "Invalid system time, not syncing to RTC."); + return; + } + ds3231_.reg.year = (now.year - 2000) % 10; + ds3231_.reg.year_10 = (now.year - 2000) / 10 % 10; + ds3231_.reg.month = now.month % 10; + ds3231_.reg.month_10 = now.month / 10; + ds3231_.reg.day = now.day_of_month % 10; + ds3231_.reg.day_10 = now.day_of_month / 10; + ds3231_.reg.weekday = now.day_of_week; + ds3231_.reg.hour = now.hour % 10; + ds3231_.reg.hour_10 = now.hour / 10; + ds3231_.reg.minute = now.minute % 10; + ds3231_.reg.minute_10 = now.minute / 10; + ds3231_.reg.second = now.second % 10; + ds3231_.reg.second_10 = now.second / 10; + ds3231_.reg.ch = false; + // ESPHOME emits warning about synchronous mode; must check datasheet to see if this is a problem for us. Datasheet pg. 6 says "The DS3231 is designed to operate in a synchronous mode, where the timekeeping registers are updated at the end of each second. This ensures that the timekeeping registers always contain valid data and eliminates the need for software to handle potential timing issues when reading or writing the registers." + this->write_rtc_(); +} + +bool DS3231Component::read_rtc_() { + if (!this->read_bytes(0, this->ds3231_.raw, sizeof(this->ds3231_.raw))) { + ESP_LOGE(TAG, "Can't read I2C data."); + return false; + } + ESP_LOGD(TAG, "Read %0u%0u:%0u%0u:%0u%0u 20%0u%0u-%0u%0u-%0u%0u CH:%s RS:%0u SQWE:%s OUT:%s", ds3231_.reg.hour_10, + ds3231_.reg.hour, ds3231_.reg.minute_10, ds3231_.reg.minute, ds3231_.reg.second_10, ds3231_.reg.second, + ds3231_.reg.year_10, ds3231_.reg.year, ds3231_.reg.month_10, ds3231_.reg.month, ds3231_.reg.day_10, + ds3231_.reg.day, ONOFF(ds3231_.reg.ch), ds3231_.reg.rs, ONOFF(ds3231_.reg.sqwe), ONOFF(ds3231_.reg.out)); + + return true; +} + +bool DS3231Component::write_rtc_() { + if (!this->write_bytes(0, this->ds3231_.raw, sizeof(this->ds3231_.raw))) { + ESP_LOGE(TAG, "Can't write I2C data."); + return false; + } + ESP_LOGD(TAG, "Write %0u%0u:%0u%0u:%0u%0u 20%0u%0u-%0u%0u-%0u%0u CH:%s RS:%0u SQWE:%s OUT:%s", ds3231_.reg.hour_10, + ds3231_.reg.hour, ds3231_.reg.minute_10, ds3231_.reg.minute, ds3231_.reg.second_10, ds3231_.reg.second, + ds3231_.reg.year_10, ds3231_.reg.year, ds3231_.reg.month_10, ds3231_.reg.month, ds3231_.reg.day_10, + ds3231_.reg.day, ONOFF(ds3231_.reg.ch), ds3231_.reg.rs, ONOFF(ds3231_.reg.sqwe), ONOFF(ds3231_.reg.out)); + return true; +} +} // namespace ds3231 +} // namespace esphome diff --git a/ds3231/ds3231.h b/ds3231/ds3231.h new file mode 100644 index 0000000..da8352a --- /dev/null +++ b/ds3231/ds3231.h @@ -0,0 +1,70 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome { +namespace ds3231 { + +class DS3231Component : public time::RealTimeClock, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override; + void read_time(); + void write_time(); + + protected: + bool read_rtc_(); + bool write_rtc_(); + union DS3231Reg { + struct { + uint8_t second : 4; + uint8_t second_10 : 3; + bool ch : 1; + + uint8_t minute : 4; + uint8_t minute_10 : 3; + uint8_t unused_1 : 1; + + uint8_t hour : 4; + uint8_t hour_10 : 2; + uint8_t unused_2 : 2; + + uint8_t weekday : 3; + uint8_t unused_3 : 5; + + uint8_t day : 4; + uint8_t day_10 : 2; + uint8_t unused_4 : 2; + + uint8_t month : 4; + uint8_t month_10 : 1; + uint8_t unused_5 : 3; + + uint8_t year : 4; + uint8_t year_10 : 4; + + uint8_t rs : 2; + uint8_t unused_6 : 2; + bool sqwe : 1; + uint8_t unused_7 : 2; + bool out : 1; + } reg; + mutable uint8_t raw[sizeof(reg)]; + } ds3231_; +}; + +template class WriteAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->write_time(); } +}; + +template class ReadAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->read_time(); } +}; +} // namespace ds3231 +} // namespace esphome diff --git a/ds3231/time.py b/ds3231/time.py new file mode 100644 index 0000000..0f57c39 --- /dev/null +++ b/ds3231/time.py @@ -0,0 +1,58 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import i2c, time +import esphome.config_validation as cv +from esphome.const import CONF_ID + +# adapted from DS1307 code by badbadc0ffee +CODEOWNERS = ["@stuurmcp"] +DEPENDENCIES = ["i2c"] +ds3231_ns = cg.esphome_ns.namespace("ds3231") +DS3231Component = ds3231_ns.class_("DS3231Component", time.RealTimeClock, i2c.I2CDevice) +WriteAction = ds3231_ns.class_("WriteAction", automation.Action) +ReadAction = ds3231_ns.class_("ReadAction", automation.Action) + + +CONFIG_SCHEMA = time.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DS3231Component), + } +).extend(i2c.i2c_device_schema(0x68)) + + +@automation.register_action( + "ds3231.write_time", + WriteAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(DS3231Component), + } + ), +) +async def ds3231_write_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "ds3231.read_time", + ReadAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(DS3231Component), + } + ), +) +async def ds3231_read_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await time.register_time(var, config) diff --git a/mcp2515/mcp2515.cpp b/mcp2515/mcp2515.cpp new file mode 100644 index 0000000..c2db922 --- /dev/null +++ b/mcp2515/mcp2515.cpp @@ -0,0 +1,711 @@ +#include "mcp2515.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp2515 { + +static const char *const TAG = "mcp2515"; + +const struct MCP2515::TxBnRegs MCP2515::TXB[N_TXBUFFERS] = {{MCP_TXB0CTRL, MCP_TXB0SIDH, MCP_TXB0DATA}, + {MCP_TXB1CTRL, MCP_TXB1SIDH, MCP_TXB1DATA}, + {MCP_TXB2CTRL, MCP_TXB2SIDH, MCP_TXB2DATA}}; + +const struct MCP2515::RxBnRegs MCP2515::RXB[N_RXBUFFERS] = {{MCP_RXB0CTRL, MCP_RXB0SIDH, MCP_RXB0DATA, CANINTF_RX0IF}, + {MCP_RXB1CTRL, MCP_RXB1SIDH, MCP_RXB1DATA, CANINTF_RX1IF}}; + +bool MCP2515::setup_internal() { + this->spi_setup(); + + if (this->reset_() != canbus::ERROR_OK) + return false; + if (this->set_bitrate_(this->bit_rate_, this->mcp_clock_) != canbus::ERROR_OK) + return false; + + // setup hardware filter RXF0 accepting all standard CAN IDs + if (this->set_filter_(RXF::RXF0, false, 0) != canbus::ERROR_OK) { + return false; + } + if (this->set_filter_mask_(MASK::MASK0, false, 0) != canbus::ERROR_OK) { + return false; + } + + // setup hardware filter RXF1 accepting all extended CAN IDs + if (this->set_filter_(RXF::RXF1, true, 0) != canbus::ERROR_OK) { + return false; + } + if (this->set_filter_mask_(MASK::MASK1, true, 0) != canbus::ERROR_OK) { + return false; + } + + if (this->set_mode_(this->mcp_mode_) != canbus::ERROR_OK) + return false; + uint8_t err_flags = this->get_error_flags_(); + ESP_LOGD(TAG, "mcp2515 setup done, error_flags = %02X", err_flags); + return true; +} + +canbus::Error MCP2515::reset_() { + this->enable(); + this->transfer_byte(INSTRUCTION_RESET); + this->disable(); + ESP_LOGV(TAG, "reset_()"); + delay(10); + + ESP_LOGV(TAG, "reset() CLEAR ALL TXB registers"); + + uint8_t zeros[14]; + memset(zeros, 0, sizeof(zeros)); + set_registers_(MCP_TXB0CTRL, zeros, 14); + set_registers_(MCP_TXB1CTRL, zeros, 14); + set_registers_(MCP_TXB2CTRL, zeros, 14); + ESP_LOGV(TAG, "reset() CLEARED TXB registers"); + + set_register_(MCP_RXB0CTRL, 0); + set_register_(MCP_RXB1CTRL, 0); + + set_register_(MCP_CANINTE, CANINTF_RX0IF | CANINTF_RX1IF | CANINTF_ERRIF | CANINTF_MERRF); + + modify_register_(MCP_RXB0CTRL, RXB_CTRL_RXM_MASK | RXB_0_CTRL_BUKT, RXB_CTRL_RXM_STDEXT | RXB_0_CTRL_BUKT); + modify_register_(MCP_RXB1CTRL, RXB_CTRL_RXM_MASK, RXB_CTRL_RXM_STDEXT); + + return canbus::ERROR_OK; +} + +uint8_t MCP2515::read_register_(const REGISTER reg) { + this->enable(); + this->transfer_byte(INSTRUCTION_READ); + this->transfer_byte(reg); + uint8_t ret = this->transfer_byte(0x00); + this->disable(); + + return ret; +} + +void MCP2515::read_registers_(const REGISTER reg, uint8_t values[], const uint8_t n) { + this->enable(); + this->transfer_byte(INSTRUCTION_READ); + this->transfer_byte(reg); + // this->transfer_array(values, n); + // mcp2515 has auto - increment of address - pointer + for (uint8_t i = 0; i < n; i++) { + values[i] = this->transfer_byte(0x00); + } + this->disable(); +} + +void MCP2515::set_register_(const REGISTER reg, const uint8_t value) { + this->enable(); + this->transfer_byte(INSTRUCTION_WRITE); + this->transfer_byte(reg); + this->transfer_byte(value); + this->disable(); +} + +void MCP2515::set_registers_(const REGISTER reg, uint8_t values[], const uint8_t n) { + this->enable(); + this->transfer_byte(INSTRUCTION_WRITE); + this->transfer_byte(reg); + // this->transfer_array(values, n); + for (uint8_t i = 0; i < n; i++) { + this->transfer_byte(values[i]); + } + this->disable(); +} + +void MCP2515::modify_register_(const REGISTER reg, const uint8_t mask, const uint8_t data) { + this->enable(); + this->transfer_byte(INSTRUCTION_BITMOD); + this->transfer_byte(reg); + this->transfer_byte(mask); + this->transfer_byte(data); + this->disable(); +} + +uint8_t MCP2515::get_status_() { + this->enable(); + this->transfer_byte(INSTRUCTION_READ_STATUS); + uint8_t i = this->transfer_byte(0x00); + this->disable(); + + return i; +} + +canbus::Error MCP2515::set_mode_(const CanctrlReqopMode mode) { + modify_register_(MCP_CANCTRL, CANCTRL_REQOP, mode); + + uint32_t start_time = millis(); + while (millis() - start_time < 10) { + if ((read_register_(MCP_CANSTAT) & CANSTAT_OPMOD) == mode) + return canbus::ERROR_OK; + } + ESP_LOGE(TAG, "Failed to set mode"); + return canbus::ERROR_FAIL; +} + +canbus::Error MCP2515::set_clk_out_(const CanClkOut divisor) { + if (divisor == CLKOUT_DISABLE) { + /* Turn off CLKEN */ + modify_register_(MCP_CANCTRL, CANCTRL_CLKEN, 0x00); + + /* Turn on CLKOUT for SOF */ + modify_register_(MCP_CNF3, CNF3_SOF, CNF3_SOF); + return canbus::ERROR_OK; + } + + /* Set the prescaler (CLKPRE) */ + modify_register_(MCP_CANCTRL, CANCTRL_CLKPRE, divisor); + + /* Turn on CLKEN */ + modify_register_(MCP_CANCTRL, CANCTRL_CLKEN, CANCTRL_CLKEN); + + /* Turn off CLKOUT for SOF */ + modify_register_(MCP_CNF3, CNF3_SOF, 0x00); + return canbus::ERROR_OK; +} + +void MCP2515::prepare_id_(uint8_t *buffer, const bool extended, const uint32_t id) { + uint16_t canid = (uint16_t) (id & 0x0FFFF); + + if (extended) { + buffer[MCP_EID0] = (uint8_t) (canid & 0xFF); + buffer[MCP_EID8] = (uint8_t) (canid >> 8); + canid = (uint16_t) (id >> 16); + buffer[MCP_SIDL] = (uint8_t) (canid & 0x03); + buffer[MCP_SIDL] += (uint8_t) ((canid & 0x1C) << 3); + buffer[MCP_SIDL] |= SIDL_EXIDE_MASK; + buffer[MCP_SIDH] = (uint8_t) (canid >> 5); + } else { + buffer[MCP_SIDH] = (uint8_t) (canid >> 3); + buffer[MCP_SIDL] = (uint8_t) ((canid & 0x07) << 5); + buffer[MCP_EID0] = 0; + buffer[MCP_EID8] = 0; + } +} + +canbus::Error MCP2515::set_filter_mask_(const MASK mask, const bool extended, const uint32_t ul_data) { + canbus::Error res = set_mode_(CANCTRL_REQOP_CONFIG); + if (res != canbus::ERROR_OK) { + return res; + } + + uint8_t tbufdata[4]; + prepare_id_(tbufdata, extended, ul_data); + + REGISTER reg; + switch (mask) { + case MASK0: + reg = MCP_RXM0SIDH; + break; + case MASK1: + reg = MCP_RXM1SIDH; + break; + default: + return canbus::ERROR_FAIL; + } + + set_registers_(reg, tbufdata, 4); + + return canbus::ERROR_OK; +} + +canbus::Error MCP2515::set_filter_(const RXF num, const bool extended, const uint32_t ul_data) { + canbus::Error res = set_mode_(CANCTRL_REQOP_CONFIG); + if (res != canbus::ERROR_OK) { + return res; + } + + REGISTER reg; + + switch (num) { + case RXF0: + reg = MCP_RXF0SIDH; + break; + case RXF1: + reg = MCP_RXF1SIDH; + break; + case RXF2: + reg = MCP_RXF2SIDH; + break; + case RXF3: + reg = MCP_RXF3SIDH; + break; + case RXF4: + reg = MCP_RXF4SIDH; + break; + case RXF5: + reg = MCP_RXF5SIDH; + break; + default: + return canbus::ERROR_FAIL; + } + + uint8_t tbufdata[4]; + prepare_id_(tbufdata, extended, ul_data); + set_registers_(reg, tbufdata, 4); + + return canbus::ERROR_OK; +} + +canbus::Error MCP2515::send_message_(TXBn txbn, struct canbus::CanFrame *frame) { + const struct TxBnRegs *txbuf = &TXB[txbn]; + + uint8_t data[13]; + + prepare_id_(data, frame->use_extended_id, frame->can_id); + data[MCP_DLC] = + frame->remote_transmission_request ? (frame->can_data_length_code | RTR_MASK) : frame->can_data_length_code; + memcpy(&data[MCP_DATA], frame->data, frame->can_data_length_code); + set_registers_(txbuf->SIDH, data, 5 + frame->can_data_length_code); + modify_register_(txbuf->CTRL, TXB_TXREQ, TXB_TXREQ); + + return canbus::ERROR_OK; +} + +canbus::Error MCP2515::send_message(struct canbus::CanFrame *frame) { + if (frame->can_data_length_code > canbus::CAN_MAX_DATA_LENGTH) { + return canbus::ERROR_FAILTX; + } + TXBn tx_buffers[N_TXBUFFERS] = {TXB0, TXB1, TXB2}; + + for (auto &tx_buffer : tx_buffers) { + const struct TxBnRegs *txbuf = &TXB[tx_buffer]; + uint8_t ctrlval = read_register_(txbuf->CTRL); + if ((ctrlval & TXB_TXREQ) == 0) { + return send_message_(tx_buffer, frame); + } + } + + return canbus::ERROR_ALLTXBUSY; +} + +canbus::Error MCP2515::read_message_(RXBn rxbn, struct canbus::CanFrame *frame) { + const struct RxBnRegs *rxb = &RXB[rxbn]; + + uint8_t tbufdata[5]; + + read_registers_(rxb->SIDH, tbufdata, 5); + + uint32_t id = (tbufdata[MCP_SIDH] << 3) + (tbufdata[MCP_SIDL] >> 5); + bool use_extended_id = false; + bool remote_transmission_request = false; + + if ((tbufdata[MCP_SIDL] & SIDL_EXIDE_MASK) == SIDL_EXIDE_MASK) { + id = (id << 2) + (tbufdata[MCP_SIDL] & 0x03); + id = (id << 8) + tbufdata[MCP_EID8]; + id = (id << 8) + tbufdata[MCP_EID0]; + // id |= canbus::CAN_EFF_FLAG; + use_extended_id = true; + } + + uint8_t dlc = (tbufdata[MCP_DLC] & DLC_MASK); + if (dlc > canbus::CAN_MAX_DATA_LENGTH) { + return canbus::ERROR_FAIL; + } + + uint8_t ctrl = read_register_(rxb->CTRL); + if (ctrl & RXB_CTRL_RTR) { + // id |= canbus::CAN_RTR_FLAG; + remote_transmission_request = true; + } + + frame->can_id = id; + frame->can_data_length_code = dlc; + frame->use_extended_id = use_extended_id; + frame->remote_transmission_request = remote_transmission_request; + + read_registers_(rxb->DATA, frame->data, dlc); + + modify_register_(MCP_CANINTF, rxb->CANINTF_RXnIF, 0); + + return canbus::ERROR_OK; +} + +canbus::Error MCP2515::read_message(struct canbus::CanFrame *frame) { + canbus::Error rc; + uint8_t stat = get_status_(); + + if (stat & STAT_RX0IF) { + rc = read_message_(RXB0, frame); + } else if (stat & STAT_RX1IF) { + rc = read_message_(RXB1, frame); + } else { + rc = canbus::ERROR_NOMSG; + } + +#ifdef ESPHOME_LOG_HAS_DEBUG + uint8_t err = get_error_flags_(); + // The receive flowchart in the datasheet says that if rollover is set (BUKT), RX1OVR flag will be set + // once both buffers are full. However, the RX0OVR flag is actually set instead. + // We can just check for both though because it doesn't break anything. + if (err & (EFLG_RX0OVR | EFLG_RX1OVR)) { + ESP_LOGD(TAG, "receive buffer overrun"); + clear_rx_n_ovr_flags_(); + } +#endif + + return rc; +} + +bool MCP2515::check_receive_() { + uint8_t res = get_status_(); + return (res & STAT_RXIF_MASK) != 0; +} + +bool MCP2515::check_error_() { + uint8_t eflg = get_error_flags_(); + return (eflg & EFLG_ERRORMASK) != 0; +} + +uint8_t MCP2515::get_error_flags_() { return read_register_(MCP_EFLG); } + +void MCP2515::clear_rx_n_ovr_flags_() { modify_register_(MCP_EFLG, EFLG_RX0OVR | EFLG_RX1OVR, 0); } + +uint8_t MCP2515::get_int_() { return read_register_(MCP_CANINTF); } + +void MCP2515::clear_int_() { set_register_(MCP_CANINTF, 0); } + +uint8_t MCP2515::get_int_mask_() { return read_register_(MCP_CANINTE); } + +void MCP2515::clear_tx_int_() { modify_register_(MCP_CANINTF, (CANINTF_TX0IF | CANINTF_TX1IF | CANINTF_TX2IF), 0); } + +void MCP2515::clear_rx_n_ovr_() { + uint8_t eflg = get_error_flags_(); + if (eflg != 0) { + clear_rx_n_ovr_flags_(); + clear_int_(); + // modify_register_(MCP_CANINTF, CANINTF_ERRIF, 0); + } +} + +void MCP2515::clear_merr_() { + // modify_register_(MCP_EFLG, EFLG_RX0OVR | EFLG_RX1OVR, 0); + // clear_int_(); + modify_register_(MCP_CANINTF, CANINTF_MERRF, 0); +} + +void MCP2515::clear_errif_() { + // modify_register_(MCP_EFLG, EFLG_RX0OVR | EFLG_RX1OVR, 0); + // clear_int_(); + modify_register_(MCP_CANINTF, CANINTF_ERRIF, 0); +} + +canbus::Error MCP2515::set_bitrate_(canbus::CanSpeed can_speed) { return this->set_bitrate_(can_speed, MCP_16MHZ); } + +canbus::Error MCP2515::set_bitrate_(canbus::CanSpeed can_speed, CanClock can_clock) { + canbus::Error error = set_mode_(CANCTRL_REQOP_CONFIG); + if (error != canbus::ERROR_OK) { + return error; + } + + uint8_t set, cfg1, cfg2, cfg3; + set = 1; + switch (can_clock) { + case (MCP_8MHZ): + switch (can_speed) { + case (canbus::CAN_5KBPS): // 5KBPS + cfg1 = MCP_8MHZ_5KBPS_CFG1; + cfg2 = MCP_8MHZ_5KBPS_CFG2; + cfg3 = MCP_8MHZ_5KBPS_CFG3; + break; + case (canbus::CAN_10KBPS): // 10KBPS + cfg1 = MCP_8MHZ_10KBPS_CFG1; + cfg2 = MCP_8MHZ_10KBPS_CFG2; + cfg3 = MCP_8MHZ_10KBPS_CFG3; + break; + case (canbus::CAN_20KBPS): // 20KBPS + cfg1 = MCP_8MHZ_20KBPS_CFG1; + cfg2 = MCP_8MHZ_20KBPS_CFG2; + cfg3 = MCP_8MHZ_20KBPS_CFG3; + break; + case (canbus::CAN_31K25BPS): // 31.25KBPS + cfg1 = MCP_8MHZ_31K25BPS_CFG1; + cfg2 = MCP_8MHZ_31K25BPS_CFG2; + cfg3 = MCP_8MHZ_31K25BPS_CFG3; + break; + case (canbus::CAN_33KBPS): // 33.333KBPS + cfg1 = MCP_8MHZ_33K3BPS_CFG1; + cfg2 = MCP_8MHZ_33K3BPS_CFG2; + cfg3 = MCP_8MHZ_33K3BPS_CFG3; + break; + case (canbus::CAN_40KBPS): // 40Kbps + cfg1 = MCP_8MHZ_40KBPS_CFG1; + cfg2 = MCP_8MHZ_40KBPS_CFG2; + cfg3 = MCP_8MHZ_40KBPS_CFG3; + break; + case (canbus::CAN_50KBPS): // 50Kbps + cfg1 = MCP_8MHZ_50KBPS_CFG1; + cfg2 = MCP_8MHZ_50KBPS_CFG2; + cfg3 = MCP_8MHZ_50KBPS_CFG3; + break; + case (canbus::CAN_80KBPS): // 80Kbps + cfg1 = MCP_8MHZ_80KBPS_CFG1; + cfg2 = MCP_8MHZ_80KBPS_CFG2; + cfg3 = MCP_8MHZ_80KBPS_CFG3; + break; + case (canbus::CAN_100KBPS): // 100Kbps + cfg1 = MCP_8MHZ_100KBPS_CFG1; + cfg2 = MCP_8MHZ_100KBPS_CFG2; + cfg3 = MCP_8MHZ_100KBPS_CFG3; + break; + case (canbus::CAN_125KBPS): // 125Kbps + cfg1 = MCP_8MHZ_125KBPS_CFG1; + cfg2 = MCP_8MHZ_125KBPS_CFG2; + cfg3 = MCP_8MHZ_125KBPS_CFG3; + break; + case (canbus::CAN_200KBPS): // 200Kbps + cfg1 = MCP_8MHZ_200KBPS_CFG1; + cfg2 = MCP_8MHZ_200KBPS_CFG2; + cfg3 = MCP_8MHZ_200KBPS_CFG3; + break; + case (canbus::CAN_250KBPS): // 250Kbps + cfg1 = MCP_8MHZ_250KBPS_CFG1; + cfg2 = MCP_8MHZ_250KBPS_CFG2; + cfg3 = MCP_8MHZ_250KBPS_CFG3; + break; + case (canbus::CAN_500KBPS): // 500Kbps + cfg1 = MCP_8MHZ_500KBPS_CFG1; + cfg2 = MCP_8MHZ_500KBPS_CFG2; + cfg3 = MCP_8MHZ_500KBPS_CFG3; + break; + case (canbus::CAN_1000KBPS): // 1Mbps + cfg1 = MCP_8MHZ_1000KBPS_CFG1; + cfg2 = MCP_8MHZ_1000KBPS_CFG2; + cfg3 = MCP_8MHZ_1000KBPS_CFG3; + break; + default: + set = 0; + break; + } + break; + + case (MCP_12MHZ): + switch (can_speed) { + case (canbus::CAN_5KBPS): // 5Kbps + cfg1 = MCP_12MHZ_5KBPS_CFG1; + cfg2 = MCP_12MHZ_5KBPS_CFG2; + cfg3 = MCP_12MHZ_5KBPS_CFG3; + break; + case (canbus::CAN_10KBPS): // 10Kbps + cfg1 = MCP_12MHZ_10KBPS_CFG1; + cfg2 = MCP_12MHZ_10KBPS_CFG2; + cfg3 = MCP_12MHZ_10KBPS_CFG3; + break; + case (canbus::CAN_20KBPS): // 20Kbps + cfg1 = MCP_12MHZ_20KBPS_CFG1; + cfg2 = MCP_12MHZ_20KBPS_CFG2; + cfg3 = MCP_12MHZ_20KBPS_CFG3; + break; + case (canbus::CAN_33KBPS): // 33.333Kbps + cfg1 = MCP_12MHZ_33K3BPS_CFG1; + cfg2 = MCP_12MHZ_33K3BPS_CFG2; + cfg3 = MCP_12MHZ_33K3BPS_CFG3; + break; + case (canbus::CAN_40KBPS): // 40Kbps + cfg1 = MCP_12MHZ_40KBPS_CFG1; + cfg2 = MCP_12MHZ_40KBPS_CFG2; + cfg3 = MCP_12MHZ_40KBPS_CFG3; + break; + case (canbus::CAN_50KBPS): // 50Kbps + cfg1 = MCP_12MHZ_50KBPS_CFG1; + cfg2 = MCP_12MHZ_50KBPS_CFG2; + cfg3 = MCP_12MHZ_50KBPS_CFG3; + break; + case (canbus::CAN_80KBPS): // 80Kbps + cfg1 = MCP_12MHZ_80KBPS_CFG1; + cfg2 = MCP_12MHZ_80KBPS_CFG2; + cfg3 = MCP_12MHZ_80KBPS_CFG3; + break; + case (canbus::CAN_100KBPS): // 100Kbps + cfg1 = MCP_12MHZ_100KBPS_CFG1; + cfg2 = MCP_12MHZ_100KBPS_CFG2; + cfg3 = MCP_12MHZ_100KBPS_CFG3; + break; + case (canbus::CAN_125KBPS): // 125Kbps + cfg1 = MCP_12MHZ_125KBPS_CFG1; + cfg2 = MCP_12MHZ_125KBPS_CFG2; + cfg3 = MCP_12MHZ_125KBPS_CFG3; + break; + case (canbus::CAN_200KBPS): // 200Kbps + cfg1 = MCP_12MHZ_200KBPS_CFG1; + cfg2 = MCP_12MHZ_200KBPS_CFG2; + cfg3 = MCP_12MHZ_200KBPS_CFG3; + break; + case (canbus::CAN_250KBPS): // 250Kbps + cfg1 = MCP_12MHZ_250KBPS_CFG1; + cfg2 = MCP_12MHZ_250KBPS_CFG2; + cfg3 = MCP_12MHZ_250KBPS_CFG3; + break; + case (canbus::CAN_500KBPS): // 500Kbps + cfg1 = MCP_12MHZ_500KBPS_CFG1; + cfg2 = MCP_12MHZ_500KBPS_CFG2; + cfg3 = MCP_12MHZ_500KBPS_CFG3; + break; + case (canbus::CAN_1000KBPS): // 1Mbps + cfg1 = MCP_12MHZ_1000KBPS_CFG1; + cfg2 = MCP_12MHZ_1000KBPS_CFG2; + cfg3 = MCP_12MHZ_1000KBPS_CFG3; + break; + default: + set = 0; + break; + } + break; + + case (MCP_16MHZ): + switch (can_speed) { + case (canbus::CAN_5KBPS): // 5Kbps + cfg1 = MCP_16MHZ_5KBPS_CFG1; + cfg2 = MCP_16MHZ_5KBPS_CFG2; + cfg3 = MCP_16MHZ_5KBPS_CFG3; + break; + case (canbus::CAN_10KBPS): // 10Kbps + cfg1 = MCP_16MHZ_10KBPS_CFG1; + cfg2 = MCP_16MHZ_10KBPS_CFG2; + cfg3 = MCP_16MHZ_10KBPS_CFG3; + break; + case (canbus::CAN_20KBPS): // 20Kbps + cfg1 = MCP_16MHZ_20KBPS_CFG1; + cfg2 = MCP_16MHZ_20KBPS_CFG2; + cfg3 = MCP_16MHZ_20KBPS_CFG3; + break; + case (canbus::CAN_33KBPS): // 33.333Kbps + cfg1 = MCP_16MHZ_33K3BPS_CFG1; + cfg2 = MCP_16MHZ_33K3BPS_CFG2; + cfg3 = MCP_16MHZ_33K3BPS_CFG3; + break; + case (canbus::CAN_40KBPS): // 40Kbps + cfg1 = MCP_16MHZ_40KBPS_CFG1; + cfg2 = MCP_16MHZ_40KBPS_CFG2; + cfg3 = MCP_16MHZ_40KBPS_CFG3; + break; + case (canbus::CAN_50KBPS): // 50Kbps + cfg1 = MCP_16MHZ_50KBPS_CFG1; + cfg2 = MCP_16MHZ_50KBPS_CFG2; + cfg3 = MCP_16MHZ_50KBPS_CFG3; + break; + case (canbus::CAN_80KBPS): // 80Kbps + cfg1 = MCP_16MHZ_80KBPS_CFG1; + cfg2 = MCP_16MHZ_80KBPS_CFG2; + cfg3 = MCP_16MHZ_80KBPS_CFG3; + break; + case (canbus::CAN_83K3BPS): // 83.333Kbps + cfg1 = MCP_16MHZ_83K3BPS_CFG1; + cfg2 = MCP_16MHZ_83K3BPS_CFG2; + cfg3 = MCP_16MHZ_83K3BPS_CFG3; + break; + case (canbus::CAN_100KBPS): // 100Kbps + cfg1 = MCP_16MHZ_100KBPS_CFG1; + cfg2 = MCP_16MHZ_100KBPS_CFG2; + cfg3 = MCP_16MHZ_100KBPS_CFG3; + break; + case (canbus::CAN_125KBPS): // 125Kbps + cfg1 = MCP_16MHZ_125KBPS_CFG1; + cfg2 = MCP_16MHZ_125KBPS_CFG2; + cfg3 = MCP_16MHZ_125KBPS_CFG3; + break; + case (canbus::CAN_200KBPS): // 200Kbps + cfg1 = MCP_16MHZ_200KBPS_CFG1; + cfg2 = MCP_16MHZ_200KBPS_CFG2; + cfg3 = MCP_16MHZ_200KBPS_CFG3; + break; + case (canbus::CAN_250KBPS): // 250Kbps + cfg1 = MCP_16MHZ_250KBPS_CFG1; + cfg2 = MCP_16MHZ_250KBPS_CFG2; + cfg3 = MCP_16MHZ_250KBPS_CFG3; + break; + case (canbus::CAN_500KBPS): // 500Kbps + cfg1 = MCP_16MHZ_500KBPS_CFG1; + cfg2 = MCP_16MHZ_500KBPS_CFG2; + cfg3 = MCP_16MHZ_500KBPS_CFG3; + break; + case (canbus::CAN_1000KBPS): // 1Mbps + cfg1 = MCP_16MHZ_1000KBPS_CFG1; + cfg2 = MCP_16MHZ_1000KBPS_CFG2; + cfg3 = MCP_16MHZ_1000KBPS_CFG3; + break; + default: + set = 0; + break; + } + break; + + case (MCP_20MHZ): + switch (can_speed) { + case (canbus::CAN_33KBPS): // 33.333Kbps + cfg1 = MCP_20MHZ_33K3BPS_CFG1; + cfg2 = MCP_20MHZ_33K3BPS_CFG2; + cfg3 = MCP_20MHZ_33K3BPS_CFG3; + break; + case (canbus::CAN_40KBPS): // 40Kbps + cfg1 = MCP_20MHZ_40KBPS_CFG1; + cfg2 = MCP_20MHZ_40KBPS_CFG2; + cfg3 = MCP_20MHZ_40KBPS_CFG3; + break; + case (canbus::CAN_50KBPS): // 50Kbps + cfg1 = MCP_20MHZ_50KBPS_CFG1; + cfg2 = MCP_20MHZ_50KBPS_CFG2; + cfg3 = MCP_20MHZ_50KBPS_CFG3; + break; + case (canbus::CAN_80KBPS): // 80Kbps + cfg1 = MCP_20MHZ_80KBPS_CFG1; + cfg2 = MCP_20MHZ_80KBPS_CFG2; + cfg3 = MCP_20MHZ_80KBPS_CFG3; + break; + case (canbus::CAN_83K3BPS): // 83.333Kbps + cfg1 = MCP_20MHZ_83K3BPS_CFG1; + cfg2 = MCP_20MHZ_83K3BPS_CFG2; + cfg3 = MCP_20MHZ_83K3BPS_CFG3; + break; + case (canbus::CAN_100KBPS): // 100Kbps + cfg1 = MCP_20MHZ_100KBPS_CFG1; + cfg2 = MCP_20MHZ_100KBPS_CFG2; + cfg3 = MCP_20MHZ_100KBPS_CFG3; + break; + case (canbus::CAN_125KBPS): // 125Kbps + cfg1 = MCP_20MHZ_125KBPS_CFG1; + cfg2 = MCP_20MHZ_125KBPS_CFG2; + cfg3 = MCP_20MHZ_125KBPS_CFG3; + break; + case (canbus::CAN_200KBPS): // 200Kbps + cfg1 = MCP_20MHZ_200KBPS_CFG1; + cfg2 = MCP_20MHZ_200KBPS_CFG2; + cfg3 = MCP_20MHZ_200KBPS_CFG3; + break; + case (canbus::CAN_250KBPS): // 250Kbps + cfg1 = MCP_20MHZ_250KBPS_CFG1; + cfg2 = MCP_20MHZ_250KBPS_CFG2; + cfg3 = MCP_20MHZ_250KBPS_CFG3; + break; + case (canbus::CAN_500KBPS): // 500Kbps + cfg1 = MCP_20MHZ_500KBPS_CFG1; + cfg2 = MCP_20MHZ_500KBPS_CFG2; + cfg3 = MCP_20MHZ_500KBPS_CFG3; + break; + case (canbus::CAN_1000KBPS): // 1Mbps + cfg1 = MCP_20MHZ_1000KBPS_CFG1; + cfg2 = MCP_20MHZ_1000KBPS_CFG2; + cfg3 = MCP_20MHZ_1000KBPS_CFG3; + break; + default: + set = 0; + break; + } + break; + + default: + set = 0; + break; + } + + if (set) { + set_register_(MCP_CNF1, cfg1); // NOLINT + set_register_(MCP_CNF2, cfg2); // NOLINT + set_register_(MCP_CNF3, cfg3); // NOLINT + return canbus::ERROR_OK; + } else { + ESP_LOGE(TAG, "Invalid frequency/bitrate combination: %d/%d", can_clock, can_speed); + return canbus::ERROR_FAIL; + } +} +} // namespace mcp2515 +} // namespace esphome diff --git a/mcp2515/mcp2515.h b/mcp2515/mcp2515.h new file mode 100644 index 0000000..c77480c --- /dev/null +++ b/mcp2515/mcp2515.h @@ -0,0 +1,112 @@ +#pragma once + +#include "esphome/components/canbus/canbus.h" +#include "esphome/components/spi/spi.h" +#include "esphome/core/component.h" +#include "mcp2515_defs.h" + +namespace esphome { +namespace mcp2515 { +static const uint32_t SPI_CLOCK = 10000000; // 10MHz + +static const int N_TXBUFFERS = 3; +static const int N_RXBUFFERS = 2; +enum CanClock { MCP_20MHZ, MCP_16MHZ, MCP_12MHZ, MCP_8MHZ }; +enum MASK { MASK0, MASK1 }; +enum RXF { RXF0 = 0, RXF1 = 1, RXF2 = 2, RXF3 = 3, RXF4 = 4, RXF5 = 5 }; +enum RXBn { RXB0 = 0, RXB1 = 1 }; +enum TXBn { TXB0 = 0, TXB1 = 1, TXB2 = 2 }; + +enum CanClkOut { + CLKOUT_DISABLE = -1, + CLKOUT_DIV1 = 0x0, + CLKOUT_DIV2 = 0x1, + CLKOUT_DIV4 = 0x2, + CLKOUT_DIV8 = 0x3, +}; + +enum CANINTF : uint8_t { + CANINTF_RX0IF = 0x01, + CANINTF_RX1IF = 0x02, + CANINTF_TX0IF = 0x04, + CANINTF_TX1IF = 0x08, + CANINTF_TX2IF = 0x10, + CANINTF_ERRIF = 0x20, + CANINTF_WAKIF = 0x40, + CANINTF_MERRF = 0x80 +}; + +enum EFLG : uint8_t { + EFLG_RX1OVR = (1 << 7), + EFLG_RX0OVR = (1 << 6), + EFLG_TXBO = (1 << 5), + EFLG_TXEP = (1 << 4), + EFLG_RXEP = (1 << 3), + EFLG_TXWAR = (1 << 2), + EFLG_RXWAR = (1 << 1), + EFLG_EWARN = (1 << 0) +}; + +enum STAT : uint8_t { STAT_RX0IF = (1 << 0), STAT_RX1IF = (1 << 1) }; + +static const uint8_t STAT_RXIF_MASK = STAT_RX0IF | STAT_RX1IF; +static const uint8_t EFLG_ERRORMASK = EFLG_RX1OVR | EFLG_RX0OVR | EFLG_TXBO | EFLG_TXEP | EFLG_RXEP; + +class MCP2515 : public canbus::Canbus, + public spi::SPIDevice { + public: + MCP2515(){}; + void set_mcp_clock(CanClock clock) { this->mcp_clock_ = clock; }; + void set_mcp_mode(const CanctrlReqopMode mode) { this->mcp_mode_ = mode; } + static const struct TxBnRegs { + REGISTER CTRL; + REGISTER SIDH; + REGISTER DATA; + } TXB[N_TXBUFFERS]; + + static const struct RxBnRegs { + REGISTER CTRL; + REGISTER SIDH; + REGISTER DATA; + CANINTF CANINTF_RXnIF; + } RXB[N_RXBUFFERS]; + + protected: + CanClock mcp_clock_{MCP_8MHZ}; + CanctrlReqopMode mcp_mode_ = CANCTRL_REQOP_NORMAL; + bool setup_internal() override; + canbus::Error set_mode_(CanctrlReqopMode mode); + + uint8_t read_register_(REGISTER reg); + void read_registers_(REGISTER reg, uint8_t values[], uint8_t n); + void set_register_(REGISTER reg, uint8_t value); + void set_registers_(REGISTER reg, uint8_t values[], uint8_t n); + void modify_register_(REGISTER reg, uint8_t mask, uint8_t data); + + void prepare_id_(uint8_t *buffer, bool extended, uint32_t id); + canbus::Error reset_(); + canbus::Error set_clk_out_(CanClkOut divisor); + canbus::Error set_bitrate_(canbus::CanSpeed can_speed); + canbus::Error set_bitrate_(canbus::CanSpeed can_speed, CanClock can_clock); + canbus::Error set_filter_mask_(MASK mask, bool extended, uint32_t ul_data); + canbus::Error set_filter_(RXF num, bool extended, uint32_t ul_data); + canbus::Error send_message_(TXBn txbn, struct canbus::CanFrame *frame); + canbus::Error send_message(struct canbus::CanFrame *frame) override; + canbus::Error read_message_(RXBn rxbn, struct canbus::CanFrame *frame); + canbus::Error read_message(struct canbus::CanFrame *frame) override; + bool check_receive_(); + bool check_error_(); + uint8_t get_error_flags_(); + void clear_rx_n_ovr_flags_(); + uint8_t get_int_(); + uint8_t get_int_mask_(); + void clear_int_(); + void clear_tx_int_(); + uint8_t get_status_(); + void clear_rx_n_ovr_(); + void clear_merr_(); + void clear_errif_(); +}; +} // namespace mcp2515 +} // namespace esphome diff --git a/mcp2515/mcp2515_defs.h b/mcp2515/mcp2515_defs.h new file mode 100644 index 0000000..b33adcb --- /dev/null +++ b/mcp2515/mcp2515_defs.h @@ -0,0 +1,375 @@ +#pragma once + +namespace esphome { +namespace mcp2515 { + +static const uint8_t CANCTRL_REQOP = 0xE0; +static const uint8_t CANCTRL_ABAT = 0x10; +static const uint8_t CANCTRL_OSM = 0x08; +static const uint8_t CANCTRL_CLKEN = 0x04; +static const uint8_t CANCTRL_CLKPRE = 0x03; + +enum CanctrlReqopMode : uint8_t { + CANCTRL_REQOP_NORMAL = 0x00, + CANCTRL_REQOP_SLEEP = 0x20, + CANCTRL_REQOP_LOOPBACK = 0x40, + CANCTRL_REQOP_LISTENONLY = 0x60, + CANCTRL_REQOP_CONFIG = 0x80, + CANCTRL_REQOP_POWERUP = 0xE0 +}; + +enum TxbNCtrl : uint8_t { + TXB_ABTF = 0x40, + TXB_MLOA = 0x20, + TXB_TXERR = 0x10, + TXB_TXREQ = 0x08, + TXB_TXIE = 0x04, + TXB_TXP = 0x03 +}; + +enum INSTRUCTION : uint8_t { + INSTRUCTION_WRITE = 0x02, + INSTRUCTION_READ = 0x03, + INSTRUCTION_BITMOD = 0x05, + INSTRUCTION_LOAD_TX0 = 0x40, + INSTRUCTION_LOAD_TX1 = 0x42, + INSTRUCTION_LOAD_TX2 = 0x44, + INSTRUCTION_RTS_TX0 = 0x81, + INSTRUCTION_RTS_TX1 = 0x82, + INSTRUCTION_RTS_TX2 = 0x84, + INSTRUCTION_RTS_ALL = 0x87, + INSTRUCTION_READ_RX0 = 0x90, + INSTRUCTION_READ_RX1 = 0x94, + INSTRUCTION_READ_STATUS = 0xA0, + INSTRUCTION_RX_STATUS = 0xB0, + INSTRUCTION_RESET = 0xC0 +}; + +enum REGISTER : uint8_t { + MCP_RXF0SIDH = 0x00, + MCP_RXF0SIDL = 0x01, + MCP_RXF0EID8 = 0x02, + MCP_RXF0EID0 = 0x03, + MCP_RXF1SIDH = 0x04, + MCP_RXF1SIDL = 0x05, + MCP_RXF1EID8 = 0x06, + MCP_RXF1EID0 = 0x07, + MCP_RXF2SIDH = 0x08, + MCP_RXF2SIDL = 0x09, + MCP_RXF2EID8 = 0x0A, + MCP_RXF2EID0 = 0x0B, + MCP_CANSTAT = 0x0E, + MCP_CANCTRL = 0x0F, + MCP_RXF3SIDH = 0x10, + MCP_RXF3SIDL = 0x11, + MCP_RXF3EID8 = 0x12, + MCP_RXF3EID0 = 0x13, + MCP_RXF4SIDH = 0x14, + MCP_RXF4SIDL = 0x15, + MCP_RXF4EID8 = 0x16, + MCP_RXF4EID0 = 0x17, + MCP_RXF5SIDH = 0x18, + MCP_RXF5SIDL = 0x19, + MCP_RXF5EID8 = 0x1A, + MCP_RXF5EID0 = 0x1B, + MCP_TEC = 0x1C, + MCP_REC = 0x1D, + MCP_RXM0SIDH = 0x20, + MCP_RXM0SIDL = 0x21, + MCP_RXM0EID8 = 0x22, + MCP_RXM0EID0 = 0x23, + MCP_RXM1SIDH = 0x24, + MCP_RXM1SIDL = 0x25, + MCP_RXM1EID8 = 0x26, + MCP_RXM1EID0 = 0x27, + MCP_CNF3 = 0x28, + MCP_CNF2 = 0x29, + MCP_CNF1 = 0x2A, + MCP_CANINTE = 0x2B, + MCP_CANINTF = 0x2C, + MCP_EFLG = 0x2D, + MCP_TXB0CTRL = 0x30, + MCP_TXB0SIDH = 0x31, + MCP_TXB0SIDL = 0x32, + MCP_TXB0EID8 = 0x33, + MCP_TXB0EID0 = 0x34, + MCP_TXB0DLC = 0x35, + MCP_TXB0DATA = 0x36, + MCP_TXB1CTRL = 0x40, + MCP_TXB1SIDH = 0x41, + MCP_TXB1SIDL = 0x42, + MCP_TXB1EID8 = 0x43, + MCP_TXB1EID0 = 0x44, + MCP_TXB1DLC = 0x45, + MCP_TXB1DATA = 0x46, + MCP_TXB2CTRL = 0x50, + MCP_TXB2SIDH = 0x51, + MCP_TXB2SIDL = 0x52, + MCP_TXB2EID8 = 0x53, + MCP_TXB2EID0 = 0x54, + MCP_TXB2DLC = 0x55, + MCP_TXB2DATA = 0x56, + MCP_RXB0CTRL = 0x60, + MCP_RXB0SIDH = 0x61, + MCP_RXB0SIDL = 0x62, + MCP_RXB0EID8 = 0x63, + MCP_RXB0EID0 = 0x64, + MCP_RXB0DLC = 0x65, + MCP_RXB0DATA = 0x66, + MCP_RXB1CTRL = 0x70, + MCP_RXB1SIDH = 0x71, + MCP_RXB1SIDL = 0x72, + MCP_RXB1EID8 = 0x73, + MCP_RXB1EID0 = 0x74, + MCP_RXB1DLC = 0x75, + MCP_RXB1DATA = 0x76 +}; + +static const uint8_t CANSTAT_OPMOD = 0xE0; +static const uint8_t CANSTAT_ICOD = 0x0E; + +static const uint8_t CNF3_SOF = 0x80; + +// applies to RXBn_SIDL, TXBn_SIDL and RXFn_SIDL +static const uint8_t SIDL_EXIDE_MASK = 0x08; + +static const uint8_t DLC_MASK = 0x0F; +static const uint8_t RTR_MASK = 0x40; + +static const uint8_t RXB_CTRL_RXM_STD = 0x20; +static const uint8_t RXB_CTRL_RXM_EXT = 0x40; +static const uint8_t RXB_CTRL_RXM_STDEXT = 0x00; +static const uint8_t RXB_CTRL_RXM_MASK = 0x60; +static const uint8_t RXB_CTRL_RTR = 0x08; +static const uint8_t RXB_0_CTRL_BUKT = 0x04; + +static const uint8_t MCP_SIDH = 0; +static const uint8_t MCP_SIDL = 1; +static const uint8_t MCP_EID8 = 2; +static const uint8_t MCP_EID0 = 3; +static const uint8_t MCP_DLC = 4; +static const uint8_t MCP_DATA = 5; + +/* + * Speed 8M + */ +static const uint8_t MCP_8MHZ_1000KBPS_CFG1 = 0x00; +static const uint8_t MCP_8MHZ_1000KBPS_CFG2 = 0x80; +static const uint8_t MCP_8MHZ_1000KBPS_CFG3 = 0x80; + +static const uint8_t MCP_8MHZ_500KBPS_CFG1 = 0x00; +static const uint8_t MCP_8MHZ_500KBPS_CFG2 = 0x90; +static const uint8_t MCP_8MHZ_500KBPS_CFG3 = 0x82; + +static const uint8_t MCP_8MHZ_250KBPS_CFG1 = 0x00; +static const uint8_t MCP_8MHZ_250KBPS_CFG2 = 0xB1; +static const uint8_t MCP_8MHZ_250KBPS_CFG3 = 0x85; + +static const uint8_t MCP_8MHZ_200KBPS_CFG1 = 0x00; +static const uint8_t MCP_8MHZ_200KBPS_CFG2 = 0xB4; +static const uint8_t MCP_8MHZ_200KBPS_CFG3 = 0x86; + +static const uint8_t MCP_8MHZ_125KBPS_CFG1 = 0x01; +static const uint8_t MCP_8MHZ_125KBPS_CFG2 = 0xB1; +static const uint8_t MCP_8MHZ_125KBPS_CFG3 = 0x85; + +static const uint8_t MCP_8MHZ_100KBPS_CFG1 = 0x01; +static const uint8_t MCP_8MHZ_100KBPS_CFG2 = 0xB4; +static const uint8_t MCP_8MHZ_100KBPS_CFG3 = 0x86; + +static const uint8_t MCP_8MHZ_80KBPS_CFG1 = 0x01; +static const uint8_t MCP_8MHZ_80KBPS_CFG2 = 0xBF; +static const uint8_t MCP_8MHZ_80KBPS_CFG3 = 0x87; + +static const uint8_t MCP_8MHZ_50KBPS_CFG1 = 0x03; +static const uint8_t MCP_8MHZ_50KBPS_CFG2 = 0xB4; +static const uint8_t MCP_8MHZ_50KBPS_CFG3 = 0x86; + +static const uint8_t MCP_8MHZ_40KBPS_CFG1 = 0x03; +static const uint8_t MCP_8MHZ_40KBPS_CFG2 = 0xBF; +static const uint8_t MCP_8MHZ_40KBPS_CFG3 = 0x87; + +static const uint8_t MCP_8MHZ_33K3BPS_CFG1 = 0x47; +static const uint8_t MCP_8MHZ_33K3BPS_CFG2 = 0xE2; +static const uint8_t MCP_8MHZ_33K3BPS_CFG3 = 0x85; + +static const uint8_t MCP_8MHZ_31K25BPS_CFG1 = 0x07; +static const uint8_t MCP_8MHZ_31K25BPS_CFG2 = 0xA4; +static const uint8_t MCP_8MHZ_31K25BPS_CFG3 = 0x84; + +static const uint8_t MCP_8MHZ_20KBPS_CFG1 = 0x07; +static const uint8_t MCP_8MHZ_20KBPS_CFG2 = 0xBF; +static const uint8_t MCP_8MHZ_20KBPS_CFG3 = 0x87; + +static const uint8_t MCP_8MHZ_10KBPS_CFG1 = 0x0F; +static const uint8_t MCP_8MHZ_10KBPS_CFG2 = 0xBF; +static const uint8_t MCP_8MHZ_10KBPS_CFG3 = 0x87; + +static const uint8_t MCP_8MHZ_5KBPS_CFG1 = 0x1F; +static const uint8_t MCP_8MHZ_5KBPS_CFG2 = 0xBF; +static const uint8_t MCP_8MHZ_5KBPS_CFG3 = 0x87; + +/* + * Speed 12M + */ + +static const uint8_t MCP_12MHZ_1000KBPS_CFG1 = 0x00; +static const uint8_t MCP_12MHZ_1000KBPS_CFG2 = 0x88; +static const uint8_t MCP_12MHZ_1000KBPS_CFG3 = 0x81; + +static const uint8_t MCP_12MHZ_500KBPS_CFG1 = 0x00; +static const uint8_t MCP_12MHZ_500KBPS_CFG2 = 0x9B; +static const uint8_t MCP_12MHZ_500KBPS_CFG3 = 0x82; + +static const uint8_t MCP_12MHZ_250KBPS_CFG1 = 0x01; +static const uint8_t MCP_12MHZ_250KBPS_CFG2 = 0x9B; +static const uint8_t MCP_12MHZ_250KBPS_CFG3 = 0x82; + +static const uint8_t MCP_12MHZ_200KBPS_CFG1 = 0x01; +static const uint8_t MCP_12MHZ_200KBPS_CFG2 = 0xA4; +static const uint8_t MCP_12MHZ_200KBPS_CFG3 = 0x83; + +static const uint8_t MCP_12MHZ_125KBPS_CFG1 = 0x03; +static const uint8_t MCP_12MHZ_125KBPS_CFG2 = 0x9B; +static const uint8_t MCP_12MHZ_125KBPS_CFG3 = 0x82; + +static const uint8_t MCP_12MHZ_100KBPS_CFG1 = 0x03; +static const uint8_t MCP_12MHZ_100KBPS_CFG2 = 0xA4; +static const uint8_t MCP_12MHZ_100KBPS_CFG3 = 0x83; + +static const uint8_t MCP_12MHZ_80KBPS_CFG1 = 0x04; +static const uint8_t MCP_12MHZ_80KBPS_CFG2 = 0xA4; +static const uint8_t MCP_12MHZ_80KBPS_CFG3 = 0x83; + +static const uint8_t MCP_12MHZ_50KBPS_CFG1 = 0x07; +static const uint8_t MCP_12MHZ_50KBPS_CFG2 = 0xA4; +static const uint8_t MCP_12MHZ_50KBPS_CFG3 = 0x83; + +static const uint8_t MCP_12MHZ_40KBPS_CFG1 = 0x09; +static const uint8_t MCP_12MHZ_40KBPS_CFG2 = 0xA4; +static const uint8_t MCP_12MHZ_40KBPS_CFG3 = 0x83; + +static const uint8_t MCP_12MHZ_33K3BPS_CFG1 = 0x08; +static const uint8_t MCP_12MHZ_33K3BPS_CFG2 = 0xB6; +static const uint8_t MCP_12MHZ_33K3BPS_CFG3 = 0x84; + +static const uint8_t MCP_12MHZ_20KBPS_CFG1 = 0x0E; +static const uint8_t MCP_12MHZ_20KBPS_CFG2 = 0xB6; +static const uint8_t MCP_12MHZ_20KBPS_CFG3 = 0x84; + +static const uint8_t MCP_12MHZ_10KBPS_CFG1 = 0x31; +static const uint8_t MCP_12MHZ_10KBPS_CFG2 = 0x9B; +static const uint8_t MCP_12MHZ_10KBPS_CFG3 = 0x82; + +static const uint8_t MCP_12MHZ_5KBPS_CFG1 = 0x3B; +static const uint8_t MCP_12MHZ_5KBPS_CFG2 = 0xB6; +static const uint8_t MCP_12MHZ_5KBPS_CFG3 = 0x84; + +/* + * speed 16M + */ +static const uint8_t MCP_16MHZ_1000KBPS_CFG1 = 0x00; +static const uint8_t MCP_16MHZ_1000KBPS_CFG2 = 0xD0; +static const uint8_t MCP_16MHZ_1000KBPS_CFG3 = 0x82; + +static const uint8_t MCP_16MHZ_500KBPS_CFG1 = 0x00; +static const uint8_t MCP_16MHZ_500KBPS_CFG2 = 0xF0; +static const uint8_t MCP_16MHZ_500KBPS_CFG3 = 0x86; + +static const uint8_t MCP_16MHZ_250KBPS_CFG1 = 0x41; +static const uint8_t MCP_16MHZ_250KBPS_CFG2 = 0xF1; +static const uint8_t MCP_16MHZ_250KBPS_CFG3 = 0x85; + +static const uint8_t MCP_16MHZ_200KBPS_CFG1 = 0x01; +static const uint8_t MCP_16MHZ_200KBPS_CFG2 = 0xFA; +static const uint8_t MCP_16MHZ_200KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_125KBPS_CFG1 = 0x03; +static const uint8_t MCP_16MHZ_125KBPS_CFG2 = 0xF0; +static const uint8_t MCP_16MHZ_125KBPS_CFG3 = 0x86; + +static const uint8_t MCP_16MHZ_100KBPS_CFG1 = 0x03; +static const uint8_t MCP_16MHZ_100KBPS_CFG2 = 0xFA; +static const uint8_t MCP_16MHZ_100KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_80KBPS_CFG1 = 0x03; +static const uint8_t MCP_16MHZ_80KBPS_CFG2 = 0xFF; +static const uint8_t MCP_16MHZ_80KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_83K3BPS_CFG1 = 0x03; +static const uint8_t MCP_16MHZ_83K3BPS_CFG2 = 0xBE; +static const uint8_t MCP_16MHZ_83K3BPS_CFG3 = 0x07; + +static const uint8_t MCP_16MHZ_50KBPS_CFG1 = 0x07; +static const uint8_t MCP_16MHZ_50KBPS_CFG2 = 0xFA; +static const uint8_t MCP_16MHZ_50KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_40KBPS_CFG1 = 0x07; +static const uint8_t MCP_16MHZ_40KBPS_CFG2 = 0xFF; +static const uint8_t MCP_16MHZ_40KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_33K3BPS_CFG1 = 0x4E; +static const uint8_t MCP_16MHZ_33K3BPS_CFG2 = 0xF1; +static const uint8_t MCP_16MHZ_33K3BPS_CFG3 = 0x85; + +static const uint8_t MCP_16MHZ_20KBPS_CFG1 = 0x0F; +static const uint8_t MCP_16MHZ_20KBPS_CFG2 = 0xFF; +static const uint8_t MCP_16MHZ_20KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_10KBPS_CFG1 = 0x1F; +static const uint8_t MCP_16MHZ_10KBPS_CFG2 = 0xFF; +static const uint8_t MCP_16MHZ_10KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_5KBPS_CFG1 = 0x3F; +static const uint8_t MCP_16MHZ_5KBPS_CFG2 = 0xFF; +static const uint8_t MCP_16MHZ_5KBPS_CFG3 = 0x87; + +/* + * speed 20M + */ +static const uint8_t MCP_20MHZ_1000KBPS_CFG1 = 0x00; +static const uint8_t MCP_20MHZ_1000KBPS_CFG2 = 0xD9; +static const uint8_t MCP_20MHZ_1000KBPS_CFG3 = 0x82; + +static const uint8_t MCP_20MHZ_500KBPS_CFG1 = 0x00; +static const uint8_t MCP_20MHZ_500KBPS_CFG2 = 0xFA; +static const uint8_t MCP_20MHZ_500KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_250KBPS_CFG1 = 0x41; +static const uint8_t MCP_20MHZ_250KBPS_CFG2 = 0xFB; +static const uint8_t MCP_20MHZ_250KBPS_CFG3 = 0x86; + +static const uint8_t MCP_20MHZ_200KBPS_CFG1 = 0x01; +static const uint8_t MCP_20MHZ_200KBPS_CFG2 = 0xFF; +static const uint8_t MCP_20MHZ_200KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_125KBPS_CFG1 = 0x03; +static const uint8_t MCP_20MHZ_125KBPS_CFG2 = 0xFA; +static const uint8_t MCP_20MHZ_125KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_100KBPS_CFG1 = 0x04; +static const uint8_t MCP_20MHZ_100KBPS_CFG2 = 0xFA; +static const uint8_t MCP_20MHZ_100KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_83K3BPS_CFG1 = 0x04; +static const uint8_t MCP_20MHZ_83K3BPS_CFG2 = 0xFE; +static const uint8_t MCP_20MHZ_83K3BPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_80KBPS_CFG1 = 0x04; +static const uint8_t MCP_20MHZ_80KBPS_CFG2 = 0xFF; +static const uint8_t MCP_20MHZ_80KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_50KBPS_CFG1 = 0x09; +static const uint8_t MCP_20MHZ_50KBPS_CFG2 = 0xFA; +static const uint8_t MCP_20MHZ_50KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_40KBPS_CFG1 = 0x09; +static const uint8_t MCP_20MHZ_40KBPS_CFG2 = 0xFF; +static const uint8_t MCP_20MHZ_40KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_33K3BPS_CFG1 = 0x0B; +static const uint8_t MCP_20MHZ_33K3BPS_CFG2 = 0xFF; +static const uint8_t MCP_20MHZ_33K3BPS_CFG3 = 0x87; + +} // namespace mcp2515 +} // namespace esphome diff --git a/rtq6056/__init__.py b/rtq6056/__init__.py new file mode 100644 index 0000000..ca1f2ca --- /dev/null +++ b/rtq6056/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@Sergio303", "@latonita"] diff --git a/rtq6056/rtq6056.cpp b/rtq6056/rtq6056.cpp new file mode 100644 index 0000000..e47a770 --- /dev/null +++ b/rtq6056/rtq6056.cpp @@ -0,0 +1,167 @@ +#include "rtq6056.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include + +namespace esphome { +namespace rtq6056 { + +static const char *const TAG = "rtq6056"; + +// | A1 | A0 | Address | +// | GND | GND | 0x40 | +// | GND | V_S+ | 0x41 | +// | GND | SDA | 0x42 | +// | GND | SCL | 0x43 | +// | V_S+ | GND | 0x44 | +// | V_S+ | V_S+ | 0x45 | +// | V_S+ | SDA | 0x46 | +// | V_S+ | SCL | 0x47 | +// | SDA | GND | 0x48 | +// | SDA | V_S+ | 0x49 | +// | SDA | SDA | 0x4A | +// | SDA | SCL | 0x4B | +// | SCL | GND | 0x4C | +// | SCL | V_S+ | 0x4D | +// | SCL | SDA | 0x4E | +// | SCL | SCL | 0x4F | + +static const uint8_t RTQ6056_REGISTER_CONFIG = 0x00; +static const uint8_t RTQ6056_REGISTER_SHUNT_VOLTAGE = 0x01; +static const uint8_t RTQ6056_REGISTER_BUS_VOLTAGE = 0x02; +static const uint8_t RTQ6056_REGISTER_POWER = 0x03; +static const uint8_t RTQ6056_REGISTER_CURRENT = 0x04; +static const uint8_t RTQ6056_REGISTER_CALIBRATION = 0x05; + +static const uint16_t RTQ6056_ADC_TIMES[] = {140, 204, 332, 588, 1100, 2116, 4156, 8244}; +static const uint16_t RTQ6056_ADC_AVG_SAMPLES[] = {1, 4, 16, 64, 128, 256, 512, 1024}; + +void RTQ6056Component::setup() { + ConfigurationRegister config; + + config.reset = 1; + if (!this->write_byte_16(RTQ6056_REGISTER_CONFIG, config.raw)) { + this->mark_failed(); + return; + } + delay(1); + + config.raw = 0; + config.reserved = 0b100; // as per datasheet + + // Averaging Mode AVG Bit Settings[11:9] (000 -> 1 sample, 001 -> 4 sample, 111 -> 1024 samples) + config.avg_samples = this->adc_avg_samples_; + + // Bus Voltage Conversion Time VBUSCT Bit Settings [8:6] (100 -> 1.1ms, 111 -> 8.244 ms) + config.bus_voltage_conversion_time = this->adc_time_voltage_; + + // Shunt Voltage Conversion Time VSHCT Bit Settings [5:3] (100 -> 1.1ms, 111 -> 8.244 ms) + config.shunt_voltage_conversion_time = this->adc_time_current_; + + // Mode Settings [2:0] Combinations (111 -> Shunt and Bus, Continuous) + config.mode = 0b111; + + if (!this->write_byte_16(RTQ6056_REGISTER_CONFIG, config.raw)) { + this->mark_failed(); + return; + } + + // lsb is multiplied by 1000000 to store it as an integer value + uint32_t lsb = static_cast(ceilf(this->max_current_a_ * 1000000.0f / 32768)); + + this->calibration_lsb_ = lsb; + + auto calibration = uint32_t(0.00512 / (lsb * this->shunt_resistance_ohm_ / 1000000.0f)); + + ESP_LOGV(TAG, " Using LSB=%" PRIu32 " calibration=%" PRIu32, lsb, calibration); + + if (!this->write_byte_16(RTQ6056_REGISTER_CALIBRATION, calibration)) { + this->mark_failed(); + return; + } +} + +void RTQ6056Component::dump_config() { + ESP_LOGCONFIG(TAG, "RTQ6056:"); + LOG_I2C_DEVICE(this); + + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + return; + } + LOG_UPDATE_INTERVAL(this); + + ESP_LOGCONFIG(TAG, + " ADC Conversion Time Bus Voltage: %d\n" + " ADC Conversion Time Shunt Voltage: %d\n" + " ADC Averaging Samples: %d", + RTQ6056_ADC_TIMES[this->adc_time_voltage_ & 0b111], RTQ6056_ADC_TIMES[this->adc_time_current_ & 0b111], + RTQ6056_ADC_AVG_SAMPLES[this->adc_avg_samples_ & 0b111]); + + LOG_SENSOR(" ", "Bus Voltage", this->bus_voltage_sensor_); + LOG_SENSOR(" ", "Shunt Voltage", this->shunt_voltage_sensor_); + LOG_SENSOR(" ", "Current", this->current_sensor_); + LOG_SENSOR(" ", "Power", this->power_sensor_); +} + +float RTQ6056Component::get_setup_priority() const { return setup_priority::DATA; } + +void RTQ6056Component::update() { + if (this->bus_voltage_sensor_ != nullptr) { + uint16_t raw_bus_voltage; + if (!this->read_byte_16(RTQ6056_REGISTER_BUS_VOLTAGE, &raw_bus_voltage)) { + this->status_set_warning(); + return; + } + // Convert for 2's compliment and signed value (though always positive) + float bus_voltage_v = this->twos_complement_(raw_bus_voltage, 16); + bus_voltage_v *= 0.00125f; + this->bus_voltage_sensor_->publish_state(bus_voltage_v); + } + + if (this->shunt_voltage_sensor_ != nullptr) { + uint16_t raw_shunt_voltage; + if (!this->read_byte_16(RTQ6056_REGISTER_SHUNT_VOLTAGE, &raw_shunt_voltage)) { + this->status_set_warning(); + return; + } + // Convert for 2's compliment and signed value + float shunt_voltage_v = this->twos_complement_(raw_shunt_voltage, 16); + shunt_voltage_v *= 0.0000025f; + this->shunt_voltage_sensor_->publish_state(shunt_voltage_v); + } + + if (this->current_sensor_ != nullptr) { + uint16_t raw_current; + if (!this->read_byte_16(RTQ6056_REGISTER_CURRENT, &raw_current)) { + this->status_set_warning(); + return; + } + // Convert for 2's compliment and signed value + float current_ma = this->twos_complement_(raw_current, 16); + current_ma *= (this->calibration_lsb_ / 1000.0f); + this->current_sensor_->publish_state(current_ma / 1000.0f); + } + + if (this->power_sensor_ != nullptr) { + uint16_t raw_power; + if (!this->read_byte_16(RTQ6056_REGISTER_POWER, &raw_power)) { + this->status_set_warning(); + return; + } + float power_mw = int16_t(raw_power) * (this->calibration_lsb_ * 25.0f / 1000.0f); + this->power_sensor_->publish_state(power_mw / 1000.0f); + } + + this->status_clear_warning(); +} + +int32_t RTQ6056Component::twos_complement_(int32_t val, uint8_t bits) { + if (val & ((uint32_t) 1 << (bits - 1))) { + val -= (uint32_t) 1 << bits; + } + return val; +} + +} // namespace rtq6056 +} // namespace esphome diff --git a/rtq6056/rtq6056.h b/rtq6056/rtq6056.h new file mode 100644 index 0000000..2216bb7 --- /dev/null +++ b/rtq6056/rtq6056.h @@ -0,0 +1,78 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace rtq6056 { + +enum AdcTime : uint16_t { + ADC_TIME_140US = 0, + ADC_TIME_204US = 1, + ADC_TIME_332US = 2, + ADC_TIME_588US = 3, + ADC_TIME_1100US = 4, + ADC_TIME_2116US = 5, + ADC_TIME_4156US = 6, + ADC_TIME_8244US = 7 +}; + +enum AdcAvgSamples : uint16_t { + ADC_AVG_SAMPLES_1 = 0, + ADC_AVG_SAMPLES_4 = 1, + ADC_AVG_SAMPLES_16 = 2, + ADC_AVG_SAMPLES_64 = 3, + ADC_AVG_SAMPLES_128 = 4, + ADC_AVG_SAMPLES_256 = 5, + ADC_AVG_SAMPLES_512 = 6, + ADC_AVG_SAMPLES_1024 = 7 +}; + +union ConfigurationRegister { + uint16_t raw; + struct { + uint16_t mode : 3; + AdcTime shunt_voltage_conversion_time : 3; + AdcTime bus_voltage_conversion_time : 3; + AdcAvgSamples avg_samples : 3; + uint16_t reserved : 3; + uint16_t reset : 1; + } __attribute__((packed)); +}; + +class RTQ6056Component : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + void set_shunt_resistance_ohm(float shunt_resistance_ohm) { shunt_resistance_ohm_ = shunt_resistance_ohm; } + void set_max_current_a(float max_current_a) { max_current_a_ = max_current_a; } + void set_adc_time_voltage(AdcTime time) { adc_time_voltage_ = time; } + void set_adc_time_current(AdcTime time) { adc_time_current_ = time; } + void set_adc_avg_samples(AdcAvgSamples samples) { adc_avg_samples_ = samples; } + + void set_bus_voltage_sensor(sensor::Sensor *bus_voltage_sensor) { bus_voltage_sensor_ = bus_voltage_sensor; } + void set_shunt_voltage_sensor(sensor::Sensor *shunt_voltage_sensor) { shunt_voltage_sensor_ = shunt_voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + + protected: + float shunt_resistance_ohm_; + float max_current_a_; + AdcTime adc_time_voltage_{AdcTime::ADC_TIME_1100US}; + AdcTime adc_time_current_{AdcTime::ADC_TIME_1100US}; + AdcAvgSamples adc_avg_samples_{AdcAvgSamples::ADC_AVG_SAMPLES_4}; + uint32_t calibration_lsb_; + sensor::Sensor *bus_voltage_sensor_{nullptr}; + sensor::Sensor *shunt_voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; + + int32_t twos_complement_(int32_t val, uint8_t bits); +}; + +} // namespace rtq6056 +} // namespace esphome diff --git a/rtq6056/sensor.py b/rtq6056/sensor.py new file mode 100644 index 0000000..e03dc8d --- /dev/null +++ b/rtq6056/sensor.py @@ -0,0 +1,147 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_BUS_VOLTAGE, + CONF_CURRENT, + CONF_ID, + CONF_MAX_CURRENT, + CONF_POWER, + CONF_SHUNT_RESISTANCE, + CONF_SHUNT_VOLTAGE, + CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_AMPERE, + UNIT_VOLT, + UNIT_WATT, +) + +DEPENDENCIES = ["i2c"] + +CONF_ADC_AVERAGING = "adc_averaging" +CONF_ADC_TIME = "adc_time" + +rtq6056_ns = cg.esphome_ns.namespace("rtq6056") +RTQ6056Component = rtq6056_ns.class_( + "RTQ6056Component", cg.PollingComponent, i2c.I2CDevice +) + +AdcTime = rtq6056_ns.enum("AdcTime") +ADC_TIMES = { + 140: AdcTime.ADC_TIME_140US, + 204: AdcTime.ADC_TIME_204US, + 332: AdcTime.ADC_TIME_332US, + 588: AdcTime.ADC_TIME_588US, + 1100: AdcTime.ADC_TIME_1100US, + 2116: AdcTime.ADC_TIME_2116US, + 4156: AdcTime.ADC_TIME_4156US, + 8244: AdcTime.ADC_TIME_8244US, +} + +AdcAvgSamples = rtq6056_ns.enum("AdcAvgSamples") +ADC_AVG_SAMPLES = { + 1: AdcAvgSamples.ADC_AVG_SAMPLES_1, + 4: AdcAvgSamples.ADC_AVG_SAMPLES_4, + 16: AdcAvgSamples.ADC_AVG_SAMPLES_16, + 64: AdcAvgSamples.ADC_AVG_SAMPLES_64, + 128: AdcAvgSamples.ADC_AVG_SAMPLES_128, + 256: AdcAvgSamples.ADC_AVG_SAMPLES_256, + 512: AdcAvgSamples.ADC_AVG_SAMPLES_512, + 1024: AdcAvgSamples.ADC_AVG_SAMPLES_1024, +} + + +def validate_adc_time(value): + value = cv.positive_time_period_microseconds(value).total_microseconds + return cv.enum(ADC_TIMES, int=True)(value) + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(RTQ6056Component), + cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All( + cv.resistance, cv.Range(min=0.0) + ), + cv.Optional(CONF_MAX_CURRENT, default=3.2): cv.All( + cv.current, cv.Range(min=0.0) + ), + cv.Optional(CONF_ADC_TIME, default="1100 us"): cv.Any( + validate_adc_time, + cv.Schema( + { + cv.Required(CONF_VOLTAGE): validate_adc_time, + cv.Required(CONF_CURRENT): validate_adc_time, + } + ), + ), + cv.Optional(CONF_ADC_AVERAGING, default=4): cv.enum( + ADC_AVG_SAMPLES, int=True + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x40)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + cg.add(var.set_shunt_resistance_ohm(config[CONF_SHUNT_RESISTANCE])) + cg.add(var.set_max_current_a(config[CONF_MAX_CURRENT])) + + adc_time_config = config[CONF_ADC_TIME] + if isinstance(adc_time_config, dict): + cg.add(var.set_adc_time_voltage(adc_time_config[CONF_VOLTAGE])) + cg.add(var.set_adc_time_current(adc_time_config[CONF_CURRENT])) + else: + cg.add(var.set_adc_time_voltage(adc_time_config)) + cg.add(var.set_adc_time_current(adc_time_config)) + + cg.add(var.set_adc_avg_samples(config[CONF_ADC_AVERAGING])) + + if CONF_BUS_VOLTAGE in config: + sens = await sensor.new_sensor(config[CONF_BUS_VOLTAGE]) + cg.add(var.set_bus_voltage_sensor(sens)) + + if CONF_SHUNT_VOLTAGE in config: + sens = await sensor.new_sensor(config[CONF_SHUNT_VOLTAGE]) + cg.add(var.set_shunt_voltage_sensor(sens)) + + if CONF_CURRENT in config: + sens = await sensor.new_sensor(config[CONF_CURRENT]) + cg.add(var.set_current_sensor(sens)) + + if CONF_POWER in config: + sens = await sensor.new_sensor(config[CONF_POWER]) + cg.add(var.set_power_sensor(sens)) diff --git a/tlc59208f_ext/__init__.py b/tlc59208f_ext/__init__.py new file mode 100644 index 0000000..f7fda8b --- /dev/null +++ b/tlc59208f_ext/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +from esphome.components import i2c +from esphome import pins +import esphome.config_validation as cv +from esphome.const import CONF_ID + +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +tlc59208f_ext_ns = cg.esphome_ns.namespace("tlc59208f_ext") +TLC59208FOutput = tlc59208f_ext_ns.class_("TLC59208FOutput", cg.Component, i2c.I2CDevice) + +CONF_RESET_PIN = "reset_pin" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(TLC59208FOutput), + cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_schema, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x20)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + reset_pin = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset_pin)) diff --git a/tlc59208f_ext/output.py b/tlc59208f_ext/output.py new file mode 100644 index 0000000..c45e3d6 --- /dev/null +++ b/tlc59208f_ext/output.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +from esphome.components import output +import esphome.config_validation as cv +from esphome.const import CONF_CHANNEL, CONF_ID + +from . import TLC59208FOutput, tlc59208f_ext_ns + +DEPENDENCIES = ["tlc59208f_ext"] + +TLC59208FChannel = tlc59208f_ext_ns.class_("TLC59208FChannel", output.FloatOutput) +CONF_TLC59208F_ID = "tlc59208f_id" + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(TLC59208FChannel), + cv.GenerateID(CONF_TLC59208F_ID): cv.use_id(TLC59208FOutput), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7), + } +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_TLC59208F_ID]) + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_channel(config[CONF_CHANNEL])) + cg.add(paren.register_channel(var)) + await output.register_output(var, config) diff --git a/tlc59208f_ext/tlc59208f_output.cpp b/tlc59208f_ext/tlc59208f_output.cpp new file mode 100644 index 0000000..3d740f0 --- /dev/null +++ b/tlc59208f_ext/tlc59208f_output.cpp @@ -0,0 +1,211 @@ +#include "tlc59208f_output.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tlc59208f_ext { + +static const char *const TAG = "tlc59208f_ext"; + +// * marks register defaults +// 0*: Register auto increment disabled, 1: Register auto increment enabled +const uint8_t TLC59208F_MODE1_AI2 = (1 << 7); +// 0*: don't auto increment bit 1, 1: auto increment bit 1 +const uint8_t TLC59208F_MODE1_AI1 = (1 << 6); +// 0*: don't auto increment bit 0, 1: auto increment bit 0 +const uint8_t TLC59208F_MODE1_AI0 = (1 << 5); +// 0: normal mode, 1*: low power mode, osc off +const uint8_t TLC59208F_MODE1_SLEEP = (1 << 4); +// 0*: device doesn't respond to i2c bus sub-address 1, 1: responds +const uint8_t TLC59208F_MODE1_SUB1 = (1 << 3); +// 0*: device doesn't respond to i2c bus sub-address 2, 1: responds +const uint8_t TLC59208F_MODE1_SUB2 = (1 << 2); +// 0*: device doesn't respond to i2c bus sub-address 3, 1: responds +const uint8_t TLC59208F_MODE1_SUB3 = (1 << 1); +// 0: device doesn't respond to i2c all-call 3, 1*: responds to all-call +const uint8_t TLC59208F_MODE1_ALLCALL = (1 << 0); + +// 0*: Group dimming, 1: Group blinking +const uint8_t TLC59208F_MODE2_DMBLNK = (1 << 5); +// 0*: Output change on Stop command, 1: Output change on ACK +const uint8_t TLC59208F_MODE2_OCH = (1 << 3); +// 0*: WDT disabled, 1: WDT enabled +const uint8_t TLC59208F_MODE2_WDTEN = (1 << 2); +// WDT timeouts +const uint8_t TLC59208F_MODE2_WDT_5MS = (0 << 0); +const uint8_t TLC59208F_MODE2_WDT_15MS = (1 << 0); +const uint8_t TLC59208F_MODE2_WDT_25MS = (2 << 0); +const uint8_t TLC59208F_MODE2_WDT_35MS = (3 << 0); + +// --- Special function --- +// Call address to perform software reset, no devices will ACK +const uint8_t TLC59208F_SWRST_ADDR = 0x96; //(0x4b 7-bit addr + ~W) +const uint8_t TLC59208F_SWRST_SEQ[2] = {0xa5, 0x5a}; + +// --- Registers ---2 +// Mode register 1 +const uint8_t TLC59208F_REG_MODE1 = 0x00; +// Mode register 2 +const uint8_t TLC59208F_REG_MODE2 = 0x01; +// PWM0 +const uint8_t TLC59208F_REG_PWM0 = 0x02; +// Group PWM +const uint8_t TLC59208F_REG_GROUPPWM = 0x0a; +// Group Freq +const uint8_t TLC59208F_REG_GROUPFREQ = 0x0b; +// LEDOUTx registers +const uint8_t TLC59208F_REG_LEDOUT0 = 0x0c; +const uint8_t TLC59208F_REG_LEDOUT1 = 0x0d; +// Sub-address registers +const uint8_t TLC59208F_REG_SUBADR1 = 0x0e; // default: 0x92 (8-bit addr) +const uint8_t TLC59208F_REG_SUBADR2 = 0x0f; // default: 0x94 (8-bit addr) +const uint8_t TLC59208F_REG_SUBADR3 = 0x10; // default: 0x98 (8-bit addr) +// All call address register +const uint8_t TLC59208F_REG_ALLCALLADR = 0x11; // default: 0xd0 (8-bit addr) + +// --- Output modes --- +static const uint8_t LDR_OFF = 0x00; +static const uint8_t LDR_ON = 0x01; +static const uint8_t LDR_PWM = 0x02; +static const uint8_t LDR_GRPPWM = 0x03; + +void TLC59208FOutput::setup() { + ESP_LOGD(TAG, " Resetting all devices on the bus"); + this->hard_reset(); + // Software reset all devices on the bus + auto errcode = this->bus_->write_readv(TLC59208F_SWRST_ADDR >> 1, TLC59208F_SWRST_SEQ, sizeof TLC59208F_SWRST_SEQ, nullptr, 0); + if (errcode != i2c::ERROR_OK) { + ESP_LOGE(TAG, "RESET failed"); + this->mark_failed(LOG_STR("RESET failed")); + return; + } + // Auto increment registers, and respond to all-call address + if (!this->write_byte(TLC59208F_REG_MODE1, TLC59208F_MODE1_AI2 | TLC59208F_MODE1_ALLCALL)) { + ESP_LOGE(TAG, "MODE1 failed"); + this->mark_failed(LOG_STR("MODE1 failed")); + return; + } + if (!this->write_byte(TLC59208F_REG_MODE2, this->mode_)) { + ESP_LOGE(TAG, "MODE2 failed"); + this->mark_failed(LOG_STR("MODE2 failed")); + return; + } + // Set all 3 outputs to be individually controlled + // TODO: think of a way to support group dimming + if (!this->write_byte(TLC59208F_REG_LEDOUT0, (LDR_PWM << 6) | (LDR_PWM << 4) | (LDR_PWM << 2) | (LDR_PWM << 0))) { + ESP_LOGE(TAG, "LEDOUT0 failed"); + this->mark_failed(LOG_STR("LEDOUT0 failed")); + return; + } + if (!this->write_byte(TLC59208F_REG_LEDOUT1, (LDR_PWM << 6) | (LDR_PWM << 4) | (LDR_PWM << 2) | (LDR_PWM << 0))) { + ESP_LOGE(TAG, "LEDOUT1 failed"); + this->mark_failed(LOG_STR("LEDOUT1 failed")); + return; + } + delayMicroseconds(500); + this->set_all_channels(255); + this->loop(); +} + +void TLC59208FOutput::hard_reset() +{ + if (this->reset_pin_ != nullptr) { + ESP_LOGV(TAG, " Performing hard reset using reset pin"); + this->reset_pin_->digital_write(false); + delayMicroseconds(10); + this->reset_pin_->digital_write(true); + delayMicroseconds(500); + } +} + +void TLC59208FOutput::dump_config() { + ESP_LOGCONFIG(TAG, + "TLC59208F_EXT:\n" + " Mode: 0x%02X", + this->mode_); + LOG_PIN(" RESET Pin:", this->reset_pin_); + if (this->is_failed()) { + ESP_LOGE(TAG, "Setting up TLC59208F failed!"); + } +} + +void TLC59208FOutput::test() +{ + ESP_LOGD(TAG, "TLC59208F_EXT: Running light test"); + this->set_all_channels(255); + this->update_ = true; + this->loop(); + delay(200); + this->set_all_channels(0); + this->update_ = true; + this->loop(); +} + +void TLC59208FOutput::serial_test() +{ + ESP_LOGD(TAG, "TLC59208F_EXT: Running serial test"); + for (uint8_t channel = this->min_channel_; channel <= this->max_channel_; channel++) { + this->set_channel_value_(channel, 255); + this->update_ = true; + this->loop(); + delay(20); + this->set_channel_value_(channel, 0); + } + for(uint8_t channel = this->max_channel_; channel >= this->min_channel_; channel--) { + this->set_channel_value_(channel, 255); + this->update_ = true; + this->loop(); + delay(20); + this->set_channel_value_(channel, 0); + } + this->update_ = true; + this->loop(); +} + +void TLC59208FOutput::set_all_channels(uint8_t value) +{ + for (uint8_t channel = this->min_channel_; channel <= this->max_channel_; channel++) { + this->set_channel_value_(channel, value); + } + this->update_ = true; +} + +void TLC59208FOutput::loop() +{ + if (this->min_channel_ == 0xFF || !this->update_) + return; + + for (uint8_t channel = this->min_channel_; channel <= this->max_channel_; channel++) { + uint8_t pwm = this->pwm_amounts_[channel]; + ESP_LOGVV(TAG, "Channel %02u: pwm=%04u ", channel, pwm); + + uint8_t reg = TLC59208F_REG_PWM0 + channel; + if (!this->write_byte(reg, pwm)) { + char buffer[128]; + snprintf(buffer, sizeof(buffer), "Writing pwm value %d to channel %d failed.", pwm, channel); + this->status_set_warning(buffer); + return; + } + } + + this->status_clear_warning(); + this->update_ = false; +} + +void TLC59208FOutput::register_channel(TLC59208FChannel *channel) { + auto c = channel->channel_; + this->min_channel_ = std::min(this->min_channel_, c); + this->max_channel_ = std::max(this->max_channel_, c); + channel->set_parent(this); +} + +void TLC59208FChannel::write_state(float state) { + const uint8_t max_duty = 255; + const float duty_rounded = roundf(state * max_duty); + auto duty = static_cast(duty_rounded); + this->parent_->set_channel_value_(this->channel_, duty); +} + +} // namespace tlc59208f_ext +} // namespace esphome diff --git a/tlc59208f_ext/tlc59208f_output.h b/tlc59208f_ext/tlc59208f_output.h new file mode 100644 index 0000000..a275219 --- /dev/null +++ b/tlc59208f_ext/tlc59208f_output.h @@ -0,0 +1,79 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/output/float_output.h" +#include "esphome/components/i2c/i2c.h" +#include + +namespace esphome { +namespace tlc59208f_ext { + +// 0*: Group dimming, 1: Group blinking +extern const uint8_t TLC59208F_MODE2_DMBLNK; +// 0*: Output change on Stop command, 1: Output change on ACK +extern const uint8_t TLC59208F_MODE2_OCH; +// 0*: WDT disabled, 1: WDT enabled +extern const uint8_t TLC59208F_MODE2_WDTEN; +// WDT timeouts +extern const uint8_t TLC59208F_MODE2_WDT_5MS; +extern const uint8_t TLC59208F_MODE2_WDT_15MS; +extern const uint8_t TLC59208F_MODE2_WDT_25MS; +extern const uint8_t TLC59208F_MODE2_WDT_35MS; + +class TLC59208FOutput; + +class TLC59208FChannel : public output::FloatOutput, public Parented { + public: + void set_channel(uint8_t channel) { channel_ = channel; } + + protected: + friend class TLC59208FOutput; + + void write_state(float state) override; + + uint8_t channel_; +}; + +/// TLC59208F float output component. +class TLC59208FOutput : public Component, public i2c::I2CDevice { + private: + void hard_reset(); + + public: + TLC59208FOutput(uint8_t mode = TLC59208F_MODE2_OCH) : mode_(mode) {} + + void register_channel(TLC59208FChannel *channel); + + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void set_reset_pin(InternalGPIOPin *pin) { this->reset_pin_ = pin; } + void loop() override; + void test(); + void serial_test(); + void set_all_channels(uint8_t value); + + protected: + friend TLC59208FChannel; + InternalGPIOPin *reset_pin_; + + void set_channel_value_(uint8_t channel, uint8_t value) { + if (this->pwm_amounts_[channel] != value) + this->update_ = true; + this->pwm_amounts_[channel] = value; + } + + + uint8_t mode_; + + uint8_t min_channel_{0xFF}; + uint8_t max_channel_{0x00}; + uint8_t pwm_amounts_[256] = { + 0, + }; + bool update_{true}; +}; + +} // namespace tlc59208f_ext +} // namespace esphome