From d1feaa935dd587910a4e3508936e85319f99eacd Mon Sep 17 00:00:00 2001 From: Arturo Casal Date: Mon, 21 Feb 2022 12:47:03 +0100 Subject: [PATCH] Add device support: MCP4728 (#3174) * Added MCP4728 output component. * Added tests to test1.yaml * Added codeowners * Lint fixes * Implemented code review changes * Lint fixes * Added i2c communication check on setup() * Fixed tests * Lint fix * Update esphome/components/mcp4728/mcp4728_output.cpp Changed log function Co-authored-by: Otto Winter Co-authored-by: Otto Winter --- CODEOWNERS | 1 + esphome/components/mcp4728/__init__.py | 29 +++++ esphome/components/mcp4728/mcp4728_output.cpp | 121 ++++++++++++++++++ esphome/components/mcp4728/mcp4728_output.h | 91 +++++++++++++ esphome/components/mcp4728/output.py | 63 +++++++++ tests/test1.yaml | 31 +++++ 6 files changed, 336 insertions(+) create mode 100644 esphome/components/mcp4728/__init__.py create mode 100644 esphome/components/mcp4728/mcp4728_output.cpp create mode 100644 esphome/components/mcp4728/mcp4728_output.h create mode 100644 esphome/components/mcp4728/output.py diff --git a/CODEOWNERS b/CODEOWNERS index eb8fb873de..a53bf63c69 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -108,6 +108,7 @@ esphome/components/mcp23x17_base/* @jesserockz esphome/components/mcp23xxx_base/* @jesserockz esphome/components/mcp2515/* @danielschramm @mvturnho esphome/components/mcp3204/* @rsumner +esphome/components/mcp4728/* @berfenger esphome/components/mcp47a1/* @jesserockz esphome/components/mcp9808/* @k7hpn esphome/components/md5/* @esphome/core diff --git a/esphome/components/mcp4728/__init__.py b/esphome/components/mcp4728/__init__.py new file mode 100644 index 0000000000..d130ceb738 --- /dev/null +++ b/esphome/components/mcp4728/__init__.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID + +CODEOWNERS = ["@berfenger"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True +CONF_STORE_IN_EEPROM = "store_in_eeprom" + +mcp4728_ns = cg.esphome_ns.namespace("mcp4728") +MCP4728Component = mcp4728_ns.class_("MCP4728Component", cg.Component, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MCP4728Component), + cv.Optional(CONF_STORE_IN_EEPROM, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x60)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID], config[CONF_STORE_IN_EEPROM]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/mcp4728/mcp4728_output.cpp b/esphome/components/mcp4728/mcp4728_output.cpp new file mode 100644 index 0000000000..d011967624 --- /dev/null +++ b/esphome/components/mcp4728/mcp4728_output.cpp @@ -0,0 +1,121 @@ +#include "mcp4728_output.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp4728 { + +static const char *const TAG = "mcp4728"; + +void MCP4728Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up MCP4728 (0x%02X)...", this->address_); + auto err = this->write(nullptr, 0); + if (err != i2c::ERROR_OK) { + this->mark_failed(); + return; + } +} + +void MCP4728Component::dump_config() { + ESP_LOGCONFIG(TAG, "MCP4728:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with MCP4728 failed!"); + } +} + +void MCP4728Component::loop() { + if (this->update_) { + this->update_ = false; + if (this->store_in_eeprom_) { + if (!this->seq_write_()) { + this->status_set_error(); + } else { + this->status_clear_error(); + } + } else { + if (!this->multi_write_()) { + this->status_set_error(); + } else { + this->status_clear_error(); + } + } + } +} + +void MCP4728Component::set_channel_value_(MCP4728ChannelIdx channel, uint16_t value) { + uint8_t cn = 0; + if (channel == MCP4728_CHANNEL_A) { + cn = 'A'; + } else if (channel == MCP4728_CHANNEL_B) { + cn = 'B'; + } else if (channel == MCP4728_CHANNEL_C) { + cn = 'C'; + } else { + cn = 'D'; + } + ESP_LOGV(TAG, "Setting MCP4728 channel %c to %d!", cn, value); + reg_[channel].data = value; + this->update_ = true; +} + +bool MCP4728Component::multi_write_() { + i2c::ErrorCode err[4]; + for (uint8_t i = 0; i < 4; ++i) { + uint8_t wd[3]; + wd[0] = ((uint8_t) CMD::MULTI_WRITE | (i << 1)) & 0xFE; + wd[1] = ((uint8_t) reg_[i].vref << 7) | ((uint8_t) reg_[i].pd << 5) | ((uint8_t) reg_[i].gain << 4) | + (reg_[i].data >> 8); + wd[2] = reg_[i].data & 0xFF; + err[i] = this->write(wd, sizeof(wd)); + } + bool ok = true; + for (auto &e : err) { + if (e != i2c::ERROR_OK) { + ok = false; + break; + } + } + return ok; +} + +bool MCP4728Component::seq_write_() { + uint8_t wd[9]; + wd[0] = (uint8_t) CMD::SEQ_WRITE; + for (uint8_t i = 0; i < 4; i++) { + wd[i * 2 + 1] = ((uint8_t) reg_[i].vref << 7) | ((uint8_t) reg_[i].pd << 5) | ((uint8_t) reg_[i].gain << 4) | + (reg_[i].data >> 8); + wd[i * 2 + 2] = reg_[i].data & 0xFF; + } + auto err = this->write(wd, sizeof(wd)); + return err == i2c::ERROR_OK; +} + +void MCP4728Component::select_vref_(MCP4728ChannelIdx channel, MCP4728Vref vref) { + reg_[channel].vref = vref; + + this->update_ = true; +} + +void MCP4728Component::select_power_down_(MCP4728ChannelIdx channel, MCP4728PwrDown pd) { + reg_[channel].pd = pd; + + this->update_ = true; +} + +void MCP4728Component::select_gain_(MCP4728ChannelIdx channel, MCP4728Gain gain) { + reg_[channel].gain = gain; + + this->update_ = true; +} + +void MCP4728Channel::write_state(float state) { + const uint16_t max_duty = 4095; + const float duty_rounded = roundf(state * max_duty); + auto duty = static_cast(duty_rounded); + this->parent_->set_channel_value_(this->channel_, duty); +} + +} // namespace mcp4728 +} // namespace esphome diff --git a/esphome/components/mcp4728/mcp4728_output.h b/esphome/components/mcp4728/mcp4728_output.h new file mode 100644 index 0000000000..55bcfdccb6 --- /dev/null +++ b/esphome/components/mcp4728/mcp4728_output.h @@ -0,0 +1,91 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/output/float_output.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace mcp4728 { + +enum class CMD { + FAST_WRITE = 0x00, + MULTI_WRITE = 0x40, + SINGLE_WRITE = 0x58, + SEQ_WRITE = 0x50, + SELECT_VREF = 0x80, + SELECT_GAIN = 0xC0, + SELECT_POWER_DOWN = 0xA0 +}; + +enum MCP4728Vref { MCP4728_VREF_VDD = 0, MCP4728_VREF_INTERNAL_2_8V = 1 }; + +enum MCP4728PwrDown { + MCP4728_PD_NORMAL = 0, + MCP4728_PD_GND_1KOHM = 1, + MCP4728_PD_GND_100KOHM = 2, + MCP4728_PD_GND_500KOHM = 3 +}; + +enum MCP4728Gain { MCP4728_GAIN_X1 = 0, MCP4728_GAIN_X2 = 1 }; + +enum MCP4728ChannelIdx { MCP4728_CHANNEL_A = 0, MCP4728_CHANNEL_B = 1, MCP4728_CHANNEL_C = 2, MCP4728_CHANNEL_D = 3 }; + +struct DACInputData { + MCP4728Vref vref; + MCP4728PwrDown pd; + MCP4728Gain gain; + uint16_t data; +}; + +class MCP4728Channel; + +/// MCP4728 float output component. +class MCP4728Component : public Component, public i2c::I2CDevice { + public: + MCP4728Component(bool store_in_eeprom) : store_in_eeprom_(store_in_eeprom) {} + + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void loop() override; + + protected: + friend MCP4728Channel; + void set_channel_value_(MCP4728ChannelIdx channel, uint16_t value); + bool multi_write_(); + bool seq_write_(); + void select_vref_(MCP4728ChannelIdx channel, MCP4728Vref vref); + void select_power_down_(MCP4728ChannelIdx channel, MCP4728PwrDown pd); + void select_gain_(MCP4728ChannelIdx channel, MCP4728Gain gain); + + private: + DACInputData reg_[4]; + bool store_in_eeprom_ = false; + bool update_ = false; +}; + +class MCP4728Channel : public output::FloatOutput { + public: + MCP4728Channel(MCP4728Component *parent, MCP4728ChannelIdx channel, MCP4728Vref vref, MCP4728Gain gain, + MCP4728PwrDown pwrdown) + : parent_(parent), channel_(channel), vref_(vref), gain_(gain), pwrdown_(pwrdown) { + // update VREF + parent->select_vref_(channel, vref_); + // update PD + parent->select_power_down_(channel, pwrdown_); + // update GAIN + parent->select_gain_(channel, gain_); + } + + protected: + void write_state(float state) override; + + MCP4728Component *parent_; + MCP4728ChannelIdx channel_; + MCP4728Vref vref_; + MCP4728Gain gain_; + MCP4728PwrDown pwrdown_; +}; + +} // namespace mcp4728 +} // namespace esphome diff --git a/esphome/components/mcp4728/output.py b/esphome/components/mcp4728/output.py new file mode 100644 index 0000000000..e0913ab98a --- /dev/null +++ b/esphome/components/mcp4728/output.py @@ -0,0 +1,63 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID, CONF_GAIN +from . import MCP4728Component, mcp4728_ns + +DEPENDENCIES = ["mcp4728"] + +MCP4728Channel = mcp4728_ns.class_("MCP4728Channel", output.FloatOutput) +CONF_MCP4728_ID = "mcp4728_id" +CONF_VREF = "vref" +CONF_POWER_DOWN = "power_down" + +MCP4728Vref = mcp4728_ns.enum("MCP4728Vref") +VREF_OPTIONS = { + "vdd": MCP4728Vref.MCP4728_VREF_VDD, + "internal": MCP4728Vref.MCP4728_VREF_INTERNAL_2_8V, +} + +MCP4728Gain = mcp4728_ns.enum("MCP4728Gain") +GAIN_OPTIONS = {"X1": MCP4728Gain.MCP4728_GAIN_X1, "X2": MCP4728Gain.MCP4728_GAIN_X2} + +MCP4728PwrDown = mcp4728_ns.enum("MCP4728PwrDown") +PWRDOWN_OPTIONS = { + "normal": MCP4728PwrDown.MCP4728_PD_NORMAL, + "gnd_1k": MCP4728PwrDown.MCP4728_PD_GND_1KOHM, + "gnd_100k": MCP4728PwrDown.MCP4728_PD_GND_100KOHM, + "gnd_500k": MCP4728PwrDown.MCP4728_PD_GND_500KOHM, +} + +MCP4728ChannelIdx = mcp4728_ns.enum("MCP4728ChannelIdx") +CHANNEL_OPTIONS = { + "A": MCP4728ChannelIdx.MCP4728_CHANNEL_A, + "B": MCP4728ChannelIdx.MCP4728_CHANNEL_B, + "C": MCP4728ChannelIdx.MCP4728_CHANNEL_C, + "D": MCP4728ChannelIdx.MCP4728_CHANNEL_D, +} + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(MCP4728Channel), + cv.GenerateID(CONF_MCP4728_ID): cv.use_id(MCP4728Component), + cv.Required(CONF_CHANNEL): cv.enum(CHANNEL_OPTIONS, upper=True), + cv.Optional(CONF_VREF, default="vdd"): cv.enum(VREF_OPTIONS, upper=False), + cv.Optional(CONF_POWER_DOWN, default="normal"): cv.enum( + PWRDOWN_OPTIONS, upper=False + ), + cv.Optional(CONF_GAIN, default="X1"): cv.enum(GAIN_OPTIONS, upper=True), + } +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_MCP4728_ID]) + var = cg.new_Pvariable( + config[CONF_ID], + paren, + config[CONF_CHANNEL], + config[CONF_VREF], + config[CONF_GAIN], + config[CONF_POWER_DOWN], + ) + await output.register_output(var, config) diff --git a/tests/test1.yaml b/tests/test1.yaml index 496226565a..65169e2fd9 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1501,6 +1501,28 @@ output: - platform: mcp4725 id: mcp4725_dac_output i2c_id: i2c_bus + - platform: mcp4728 + id: mcp4728_dac_output_a + channel: A + vref: vdd + power_down: normal + - platform: mcp4728 + id: mcp4728_dac_output_b + channel: B + vref: internal + gain: X1 + power_down: gnd_1k + - platform: mcp4728 + id: mcp4728_dac_output_c + channel: C + vref: vdd + power_down: gnd_100k + - platform: mcp4728 + id: mcp4728_dac_output_d + channel: D + vref: internal + gain: X2 + power_down: gnd_500k e131: @@ -2013,6 +2035,9 @@ switch: - output.set_level: id: mcp4725_dac_output level: !lambda "return 0.5;" + - output.set_level: + id: mcp4728_dac_output_a + level: !lambda "return 0.5;" turn_off_action: - switch.turn_on: living_room_lights_off restore_state: False @@ -2393,6 +2418,12 @@ rc522_i2c: ESP_LOGD("main", "Found tag %s", x.c_str()); i2c_id: i2c_bus +mcp4728: + - id: mcp4728_dac + store_in_eeprom: False + address: 0x60 + i2c_id: i2c_bus + gps: uart_id: uart0