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 <otto@otto-winter.com>

Co-authored-by: Otto Winter <otto@otto-winter.com>
This commit is contained in:
Arturo Casal 2022-02-21 12:47:03 +01:00 committed by GitHub
parent 6919930aaa
commit d1feaa935d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 336 additions and 0 deletions

View file

@ -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

View file

@ -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)

View file

@ -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<uint16_t>(duty_rounded);
this->parent_->set_channel_value_(this->channel_, duty);
}
} // namespace mcp4728
} // namespace esphome

View file

@ -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

View file

@ -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)

View file

@ -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