ESPHome tlc5971

I want to use the TLC5971 with ESPHome. I have modified the TLC5947 code to a point where it should work. I however am not sure how to implement this in the YAML.

//tlc5971.h
#pragma once
// TLC5971 12-Channel, 16-Bit PWM LED Driver

#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/output/float_output.h"
#include <vector>

#include "esphome/core/log.h"

namespace esphome {
namespace tlc5971 {

class TLC5971 : public Component {
 public:
  class Channel;

  const uint8_t N_CHANNELS_PER_CHIP = 12;

  void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; }
  void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; }
  void set_num_chips(uint8_t num_chips) { num_chips_ = num_chips; }

  void setup() override{
    this->data_pin_->setup();
    this->data_pin_->digital_write(true);
    this->clock_pin_->setup();
    this->clock_pin_->digital_write(true);

    this->pwm_amounts_.resize(this->num_chips_ * N_CHANNELS_PER_CHIP, 0);

    ESP_LOGCONFIG(TAG, "Done setting up TLC5947 output component.");
  }

  void dump_config() override {
    ESP_LOGCONFIG(TAG, "TLC5947:");
    LOG_PIN("  Data Pin: ", this->data_pin_);
    LOG_PIN("  Clock Pin: ", this->clock_pin_);
    ESP_LOGCONFIG(TAG, "  Number of chips: %u", this->num_chips_);
  }

  float get_setup_priority() const override { return setup_priority::HARDWARE; }

  /// Send new values if they were updated.
  void loop() override{
    if (!this->update_)
      return;


    // push the data out, MSB first, 12 bit word per channel, 24 channels per chip
    for (int32_t ch = num_chips_ - 1; ch >= 0; ch--) {
      uint16_t chan1 = pwm_amounts_[0 + ch * 12];
      uint16_t chan2 = pwm_amounts_[1 + ch * 12];
      uint16_t chan3 = pwm_amounts_[2 + ch * 12];
      uint16_t chan4 = pwm_amounts_[3 + ch * 12];
      uint16_t chan5 = pwm_amounts_[4 + ch * 12];
      uint16_t chan6 = pwm_amounts_[5 + ch * 12];
      uint16_t chan7 = pwm_amounts_[6 + ch * 12];
      uint16_t chan8 = pwm_amounts_[7 + ch * 12];
      uint16_t chan9 = pwm_amounts_[8 + ch * 12];
      uint16_t chan10 = pwm_amounts_[9 + ch * 12];
      uint16_t chan11 = pwm_amounts_[10 + ch * 12];
      uint16_t chan12 = pwm_amounts_[11 + ch * 12];
      

      // for (uint8_t bit = 0; bit < 12; bit++) {
      //   this->clock_pin_->digital_write(false);
      //   this->data_pin_->digital_write(word & 0x800);
      //   word <<= 1;

      //   this->clock_pin_->digital_write(true);
      //   this->clock_pin_->digital_write(true);  // TWH0>12ns, so we should be fine using this as delay
      // }

      for(int x = 0; x < 4; x++){
        this->write(data[i]);
      }

      write(chan1 >> 8);
      write(chan1 & 0xFF);
      write(chan2 >> 8);
      write(chan2 & 0xFF);
      write(chan3 >> 8);
      write(chan3 & 0xFF);
      write(chan4 >> 8);
      write(chan4 & 0xFF);
      write(chan5 >> 8);
      write(chan5 & 0xFF);
      write(chan6 >> 8);
      write(chan6 & 0xFF);
      write(chan7 >> 8);
      write(chan7 & 0xFF);
      write(chan8 >> 8);
      write(chan8 & 0xFF);
      write(chan9 >> 8);
      write(chan9 & 0xFF);
      write(chan10 >> 8);
      write(chan10 & 0xFF);
      write(chan11 >> 8);
      write(chan11 & 0xFF);
      write(chan12 >> 8);
      write(chan12 & 0xFF);
    }

    this->clock_pin_->digital_write(false);

    this->update_ = false;
  }

  class Channel : public output::FloatOutput {
   public:
    void set_parent(TLC5947 *parent) { parent_ = parent; }
    void set_channel(uint8_t channel) { channel_ = channel; }

   protected:
    void write_state(float state) override {
      auto amount = static_cast<uint16_t>(state * 0xfff);
      this->parent_->set_channel_value_(this->channel_, amount);
    }

    TLC5947 *parent_;
    uint8_t channel_;
  };

 protected:
  void set_channel_value_(uint16_t channel, uint16_t value) {
    if (channel >= this->num_chips_ * N_CHANNELS_PER_CHIP)
      return;
    if (this->pwm_amounts_[channel] != value) {
      this->update_ = true;
    }
    this->pwm_amounts_[channel] = value;
  }

  void write(byte out){
    shiftOut(data_pin_, clock_pin_, MSBFIRST, out); 
  }

  GPIOPin *data_pin_;
  GPIOPin *clock_pin_;
  uint8_t num_chips_;

  std::vector<uint16_t> pwm_amounts_;
  bool update_{true};

  char commandStart[4] = { 0x94, 0x50, 0x0F, 0xFF };
};

}  // namespace tlc5947
}  // namespace esphome
# output.py
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import output
from esphome.const import CONF_CHANNEL, CONF_ID
from . import tlc5971

DEPENDENCIES = ["esphome_tlc5971"]
CODEOWNERS = ["@ijsselk"]

Channel = tlc5971.class_("Channel", output.FloatOutput)

CONF_TLC5971_ID = "tlc5971_id"
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
    {
        cv.GenerateID(CONF_TLC5971_ID): cv.use_id(tlc5971),
        cv.Required(CONF_ID): cv.declare_id(Channel),
        cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535),
    }
).extend(cv.COMPONENT_SCHEMA)


async def to_code(config):
    var = cg.new_Pvariable(config[CONF_ID])
    await output.register_output(var, config)

    parent = await cg.get_variable(config[CONF_TLC5971_ID])
    cg.add(var.set_parent(parent))
    cg.add(var.set_channel(config[CONF_CHANNEL]))

I have also looked at the Custom Output page, but I’m not sure how to add multiple outputs in one included library. This is probably the way to do it, but using this code only registers one output and i’m unsure how to parse the channel parameter.

- platform: custom
  type: binary
  lambda: |-
    auto my_custom_binary_output = new MyCustomBinaryOutput();
    App.register_component(my_custom_binary_output);
    return {my_custom_binary_output};

  outputs:
    id: custom_binary

I am not sure how to continue, or which of the abovementioned options to pursue. Thanks for your help.

Well, some years later I have actually learned how the esphome code works lol. I have opened a pull request for the working component code.

1 Like