Add support for MCP4461 quad i2c digipot/rheostat (#8180)

Co-authored-by: Oliver Kleinecke <kleinecke.oliver@googlemail.com>
Co-authored-by: Djordje Mandic <6750655+DjordjeMandic@users.noreply.github.com>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
This commit is contained in:
Oliver Kleinecke
2025-04-01 23:55:06 +00:00
committed by GitHub
co-authored by Oliver Kleinecke Djordje Mandic Keith Burzinski
parent 0812b3dd70
commit e3eb3ee5d2
13 changed files with 1067 additions and 0 deletions
+1
View File
@@ -266,6 +266,7 @@ esphome/components/mcp23x17_base/* @jesserockz
esphome/components/mcp23xxx_base/* @jesserockz
esphome/components/mcp2515/* @danielschramm @mvturnho
esphome/components/mcp3204/* @rsumner
esphome/components/mcp4461/* @p1ngb4ck
esphome/components/mcp4728/* @berfenger
esphome/components/mcp47a1/* @jesserockz
esphome/components/mcp9600/* @mreditor97
+42
View File
@@ -0,0 +1,42 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c
from esphome.const import CONF_ID
CODEOWNERS = ["@p1ngb4ck"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
CONF_DISABLE_WIPER_0 = "disable_wiper_0"
CONF_DISABLE_WIPER_1 = "disable_wiper_1"
CONF_DISABLE_WIPER_2 = "disable_wiper_2"
CONF_DISABLE_WIPER_3 = "disable_wiper_3"
mcp4461_ns = cg.esphome_ns.namespace("mcp4461")
Mcp4461Component = mcp4461_ns.class_("Mcp4461Component", cg.Component, i2c.I2CDevice)
CONF_MCP4461_ID = "mcp4461_id"
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(Mcp4461Component),
cv.Optional(CONF_DISABLE_WIPER_0, default=False): cv.boolean,
cv.Optional(CONF_DISABLE_WIPER_1, default=False): cv.boolean,
cv.Optional(CONF_DISABLE_WIPER_2, default=False): cv.boolean,
cv.Optional(CONF_DISABLE_WIPER_3, default=False): cv.boolean,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x2C))
)
async def to_code(config):
var = cg.new_Pvariable(
config[CONF_ID],
config[CONF_DISABLE_WIPER_0],
config[CONF_DISABLE_WIPER_1],
config[CONF_DISABLE_WIPER_2],
config[CONF_DISABLE_WIPER_3],
)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
File diff suppressed because it is too large Load Diff
+171
View File
@@ -0,0 +1,171 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/log.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace mcp4461 {
struct WiperState {
bool enabled = true;
uint16_t state = 0;
optional<float> initial_value;
bool terminal_a = true;
bool terminal_b = true;
bool terminal_w = true;
bool terminal_hw = true;
bool wiper_lock_active = false;
bool update_level = false;
bool update_terminal = false;
};
// default wiper state is 128 / 0x80h
enum class Mcp4461Commands : uint8_t { WRITE = 0x00, INCREMENT = 0x04, DECREMENT = 0x08, READ = 0x0C };
enum class Mcp4461Addresses : uint8_t {
MCP4461_VW0 = 0x00,
MCP4461_VW1 = 0x10,
MCP4461_VW2 = 0x60,
MCP4461_VW3 = 0x70,
MCP4461_STATUS = 0x50,
MCP4461_TCON0 = 0x40,
MCP4461_TCON1 = 0xA0,
MCP4461_EEPROM_1 = 0xB0
};
enum class Mcp4461WiperIdx : uint8_t {
MCP4461_WIPER_0 = 0,
MCP4461_WIPER_1 = 1,
MCP4461_WIPER_2 = 2,
MCP4461_WIPER_3 = 3,
MCP4461_WIPER_4 = 4,
MCP4461_WIPER_5 = 5,
MCP4461_WIPER_6 = 6,
MCP4461_WIPER_7 = 7
};
enum class Mcp4461EepromLocation : uint8_t {
MCP4461_EEPROM_0 = 0,
MCP4461_EEPROM_1 = 1,
MCP4461_EEPROM_2 = 2,
MCP4461_EEPROM_3 = 3,
MCP4461_EEPROM_4 = 4
};
enum class Mcp4461TerminalIdx : uint8_t { MCP4461_TERMINAL_0 = 0, MCP4461_TERMINAL_1 = 1 };
class Mcp4461Wiper;
// Mcp4461Component
class Mcp4461Component : public Component, public i2c::I2CDevice {
public:
Mcp4461Component(bool disable_wiper_0, bool disable_wiper_1, bool disable_wiper_2, bool disable_wiper_3)
: wiper_0_disabled_(disable_wiper_0),
wiper_1_disabled_(disable_wiper_1),
wiper_2_disabled_(disable_wiper_2),
wiper_3_disabled_(disable_wiper_3) {
this->reg_[0].enabled = !wiper_0_disabled_;
this->reg_[1].enabled = !wiper_1_disabled_;
this->reg_[2].enabled = !wiper_2_disabled_;
this->reg_[3].enabled = !wiper_3_disabled_;
}
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void loop() override;
/// @brief get eeprom value from location
/// @param[in] location - eeprom location 0-4
/// @return eeprom value of specified location (9 bits max)
uint16_t get_eeprom_value(Mcp4461EepromLocation location);
/// @brief set eeprom value at specified location
/// @param[in] location - eeprom location 0-4
/// @param[in] value - 9 bits value to store
bool set_eeprom_value(Mcp4461EepromLocation location, uint16_t value);
/// @brief public function used to set initial value
/// @param[in] wiper - the wiper to set the value for
/// @param[in] initial_value - the initial value in range 0-1.0 as float
void set_initial_value(Mcp4461WiperIdx wiper, float initial_value);
/// @brief public function used to set disable terminal config
/// @param[in] wiper - the wiper to set the value for
/// @param[in] terminal - the terminal to disable, one of ['a','b','w','h']
void initialize_terminal_disabled(Mcp4461WiperIdx wiper, char terminal);
/// @brief read status register to log
void read_status_register_to_log();
protected:
friend class Mcp4461Wiper;
void update_write_protection_status_();
uint8_t get_wiper_address_(uint8_t wiper);
uint16_t read_wiper_level_(uint8_t wiper);
uint8_t get_status_register_();
uint16_t get_wiper_level_(Mcp4461WiperIdx wiper);
bool set_wiper_level_(Mcp4461WiperIdx wiper, uint16_t value);
bool update_wiper_level_(Mcp4461WiperIdx wiper);
void enable_wiper_(Mcp4461WiperIdx wiper);
void disable_wiper_(Mcp4461WiperIdx wiper);
bool increase_wiper_(Mcp4461WiperIdx wiper);
bool decrease_wiper_(Mcp4461WiperIdx wiper);
void enable_terminal_(Mcp4461WiperIdx wiper, char terminal);
void disable_terminal_(Mcp4461WiperIdx, char terminal);
bool is_writing_();
bool is_eeprom_ready_for_writing_(bool wait_if_not_ready);
void write_wiper_level_(uint8_t wiper, uint16_t value);
bool mcp4461_write_(uint8_t addr, uint16_t data, bool nonvolatile = false);
uint8_t calc_terminal_connector_byte_(Mcp4461TerminalIdx terminal_connector);
void update_terminal_register_(Mcp4461TerminalIdx terminal_connector);
uint8_t get_terminal_register_(Mcp4461TerminalIdx terminal_connector);
bool set_terminal_register_(Mcp4461TerminalIdx terminal_connector, uint8_t data);
// Converts a status to a human readable string
static const LogString *get_message_string(int status) {
switch (status) {
case MCP4461_STATUS_I2C_ERROR:
return LOG_STR("I2C error - communication with MCP4461 failed!");
case MCP4461_STATUS_REGISTER_ERROR:
return LOG_STR("Status register could not be read");
case MCP4461_STATUS_REGISTER_INVALID:
return LOG_STR("Invalid status register value - bits 1,7 or 8 are 0");
case MCP4461_VALUE_INVALID:
return LOG_STR("Invalid value for wiper given");
case MCP4461_WRITE_PROTECTED:
return LOG_STR("MCP4461 is write protected. Setting nonvolatile wipers/eeprom values is prohibited.");
case MCP4461_WIPER_ENABLED:
return LOG_STR("MCP4461 Wiper is already enabled, ignoring cmd to enable.");
case MCP4461_WIPER_DISABLED:
return LOG_STR("MCP4461 Wiper is disabled. All actions on this wiper are prohibited.");
case MCP4461_WIPER_LOCKED:
return LOG_STR("MCP4461 Wiper is locked using WiperLock-technology. All actions on this wiper are prohibited.");
case MCP4461_STATUS_OK:
return LOG_STR("Status OK");
default:
return LOG_STR("Unknown");
}
}
enum ErrorCode {
MCP4461_STATUS_OK = 0, // CMD completed successfully
MCP4461_FAILED, // component failed
MCP4461_STATUS_I2C_ERROR, // Unable to communicate with device
MCP4461_STATUS_REGISTER_INVALID, // Status register value was invalid
MCP4461_STATUS_REGISTER_ERROR, // Error fetching status register
MCP4461_PROHIBITED_FOR_NONVOLATILE, //
MCP4461_VALUE_INVALID, // Invalid value given for wiper / eeprom
MCP4461_WRITE_PROTECTED, // The value was read, but the CRC over the payload (valid and data) does not match
MCP4461_WIPER_ENABLED, // The wiper is enabled, discard additional enabling actions
MCP4461_WIPER_DISABLED, // The wiper is disabled - all actions for this wiper will be aborted/discarded
MCP4461_WIPER_LOCKED, // The wiper is locked using WiperLock-technology - all actions for this wiper will be
// aborted/discarded
} error_code_{MCP4461_STATUS_OK};
WiperState reg_[8];
bool last_eeprom_write_timed_out_{false};
bool write_protected_{false};
bool wiper_0_disabled_{false};
bool wiper_1_disabled_{false};
bool wiper_2_disabled_{false};
bool wiper_3_disabled_{false};
};
} // namespace mcp4461
} // namespace esphome
@@ -0,0 +1,60 @@
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_INITIAL_VALUE
from .. import Mcp4461Component, CONF_MCP4461_ID, mcp4461_ns
DEPENDENCIES = ["mcp4461"]
Mcp4461Wiper = mcp4461_ns.class_(
"Mcp4461Wiper", output.FloatOutput, cg.Parented.template(Mcp4461Component)
)
Mcp4461WiperIdx = mcp4461_ns.enum("Mcp4461WiperIdx", is_class=True)
CHANNEL_OPTIONS = {
"A": Mcp4461WiperIdx.MCP4461_WIPER_0,
"B": Mcp4461WiperIdx.MCP4461_WIPER_1,
"C": Mcp4461WiperIdx.MCP4461_WIPER_2,
"D": Mcp4461WiperIdx.MCP4461_WIPER_3,
"E": Mcp4461WiperIdx.MCP4461_WIPER_4,
"F": Mcp4461WiperIdx.MCP4461_WIPER_5,
"G": Mcp4461WiperIdx.MCP4461_WIPER_6,
"H": Mcp4461WiperIdx.MCP4461_WIPER_7,
}
CONF_TERMINAL_A = "terminal_a"
CONF_TERMINAL_B = "terminal_b"
CONF_TERMINAL_W = "terminal_w"
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(Mcp4461Wiper),
cv.GenerateID(CONF_MCP4461_ID): cv.use_id(Mcp4461Component),
cv.Required(CONF_CHANNEL): cv.enum(CHANNEL_OPTIONS, upper=True),
cv.Optional(CONF_TERMINAL_A, default=True): cv.boolean,
cv.Optional(CONF_TERMINAL_B, default=True): cv.boolean,
cv.Optional(CONF_TERMINAL_W, default=True): cv.boolean,
cv.Optional(CONF_INITIAL_VALUE): cv.float_range(min=0.0, max=1.0),
}
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_MCP4461_ID])
var = cg.new_Pvariable(
config[CONF_ID],
parent,
config[CONF_CHANNEL],
)
if not config[CONF_TERMINAL_A]:
cg.add(parent.initialize_terminal_disabled(config[CONF_CHANNEL], "a"))
if not config[CONF_TERMINAL_B]:
cg.add(parent.initialize_terminal_disabled(config[CONF_CHANNEL], "b"))
if not config[CONF_TERMINAL_W]:
cg.add(parent.initialize_terminal_disabled(config[CONF_CHANNEL], "w"))
if CONF_INITIAL_VALUE in config:
cg.add(
parent.set_initial_value(config[CONF_CHANNEL], config[CONF_INITIAL_VALUE])
)
await output.register_output(var, config)
await cg.register_parented(var, config[CONF_MCP4461_ID])
@@ -0,0 +1,73 @@
#include "mcp4461_output.h"
#include <cmath>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace mcp4461 {
static const char *const TAG = "mcp4461.output";
// public set_level function
void Mcp4461Wiper::set_level(float state) {
if (!std::isfinite(state)) {
ESP_LOGW(TAG, "Finite state state value is required.");
return;
}
state = clamp(state, 0.0f, 1.0f);
if (this->is_inverted()) {
state = 1.0f - state;
}
this->write_state(state);
}
// floats from other components (like light etc.) are passed as "percentage floats"
// this function converts them to the 0 - 256 range used by the MCP4461
void Mcp4461Wiper::write_state(float state) {
if (this->parent_->set_wiper_level_(this->wiper_, static_cast<uint16_t>(std::roundf(state * 256)))) {
this->state_ = state;
}
}
float Mcp4461Wiper::read_state() { return (static_cast<float>(this->parent_->get_wiper_level_(this->wiper_)) / 256.0); }
float Mcp4461Wiper::update_state() {
this->state_ = this->read_state();
return this->state_;
}
void Mcp4461Wiper::set_state(bool state) {
if (state) {
this->turn_on();
} else {
this->turn_off();
}
}
void Mcp4461Wiper::turn_on() { this->parent_->enable_wiper_(this->wiper_); }
void Mcp4461Wiper::turn_off() { this->parent_->disable_wiper_(this->wiper_); }
void Mcp4461Wiper::increase_wiper() {
if (this->parent_->increase_wiper_(this->wiper_)) {
this->state_ = this->update_state();
ESP_LOGV(TAG, "Increased wiper %u to %u", static_cast<uint8_t>(this->wiper_),
static_cast<uint16_t>(std::roundf(this->state_ * 256)));
}
}
void Mcp4461Wiper::decrease_wiper() {
if (this->parent_->decrease_wiper_(this->wiper_)) {
this->state_ = this->update_state();
ESP_LOGV(TAG, "Decreased wiper %u to %u", static_cast<uint8_t>(this->wiper_),
static_cast<uint16_t>(std::roundf(this->state_ * 256)));
}
}
void Mcp4461Wiper::enable_terminal(char terminal) { this->parent_->enable_terminal_(this->wiper_, terminal); }
void Mcp4461Wiper::disable_terminal(char terminal) { this->parent_->disable_terminal_(this->wiper_, terminal); }
} // namespace mcp4461
} // namespace esphome
@@ -0,0 +1,49 @@
#pragma once
#include "../mcp4461.h"
#include "esphome/core/component.h"
#include "esphome/components/output/float_output.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace mcp4461 {
class Mcp4461Wiper : public output::FloatOutput, public Parented<Mcp4461Component> {
public:
Mcp4461Wiper(Mcp4461Component *parent, Mcp4461WiperIdx wiper) : parent_(parent), wiper_(wiper) {}
/// @brief Set level of wiper
/// @param[in] state - The desired float level in range 0-1.0
void set_level(float state);
/// @brief Enables/Disables current output using bool parameter
/// @param[in] state boolean var representing desired state (true=ON, false=OFF)
void set_state(bool state) override;
/// @brief Enables current output
void turn_on() override;
/// @brief Disables current output
void turn_off() override;
/// @brief Read current device wiper state without updating internal output state
/// @return float - current device state as float in range 0 - 1.0
float read_state();
/// @brief Update current output state using device wiper state
/// @return float - current updated output state as float in range 0 - 1.0
float update_state();
/// @brief Increase wiper by 1 tap
void increase_wiper();
/// @brief Decrease wiper by 1 tap
void decrease_wiper();
/// @brief Enable given terminal
/// @param[in] terminal single char parameter defining desired terminal to enable, one of { 'a', 'b', 'w', 'h' }
void enable_terminal(char terminal);
/// @brief Disable given terminal
/// @param[in] terminal single char parameter defining desired terminal to disable, one of { 'a', 'b', 'w', 'h' }
void disable_terminal(char terminal);
protected:
void write_state(float state) override;
Mcp4461Component *parent_;
Mcp4461WiperIdx wiper_;
float state_;
};
} // namespace mcp4461
} // namespace esphome
+28
View File
@@ -0,0 +1,28 @@
i2c:
- id: i2c_mcp4461
sda: ${sda_pin}
scl: ${scl_pin}
mcp4461:
- id: mcp4461_digipot_01
output:
- platform: mcp4461
id: digipot_wiper_1
mcp4461_id: mcp4461_digipot_01
channel: A
- platform: mcp4461
id: digipot_wiper_2
mcp4461_id: mcp4461_digipot_01
channel: B
- platform: mcp4461
id: digipot_wiper_3
mcp4461_id: mcp4461_digipot_01
channel: C
- platform: mcp4461
id: digipot_wiper_4
mcp4461_id: mcp4461_digipot_01
channel: D
@@ -0,0 +1,5 @@
substitutions:
sda_pin: GPIO16
scl_pin: GPIO17
<<: !include common.yaml
@@ -0,0 +1,5 @@
substitutions:
sda_pin: GPIO4
scl_pin: GPIO5
<<: !include common.yaml
@@ -0,0 +1,5 @@
substitutions:
sda_pin: GPIO4
scl_pin: GPIO5
<<: !include common.yaml
@@ -0,0 +1,5 @@
substitutions:
sda_pin: GPIO16
scl_pin: GPIO17
<<: !include common.yaml
@@ -0,0 +1,5 @@
substitutions:
sda_pin: GPIO4
scl_pin: GPIO5
<<: !include common.yaml