mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	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:
		| @@ -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 | ||||
|   | ||||
							
								
								
									
										29
									
								
								esphome/components/mcp4728/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								esphome/components/mcp4728/__init__.py
									
									
									
									
									
										Normal 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) | ||||
							
								
								
									
										121
									
								
								esphome/components/mcp4728/mcp4728_output.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								esphome/components/mcp4728/mcp4728_output.cpp
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										91
									
								
								esphome/components/mcp4728/mcp4728_output.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								esphome/components/mcp4728/mcp4728_output.h
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										63
									
								
								esphome/components/mcp4728/output.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								esphome/components/mcp4728/output.py
									
									
									
									
									
										Normal 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) | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user