mirror of
https://github.com/esphome/esphome.git
synced 2026-10-18 20:22:49 +00:00
[fusb302b] Add FUSB302B driver (#20760)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
Claude Opus 5.5
J. Nick Koston
parent
471774cdbf
commit
eafeef7cb8
@@ -1,2 +1,40 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@remcom"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "fusb302b"
|
||||
|
||||
CONF_REQUEST_VOLTAGE = "request_voltage"
|
||||
|
||||
fusb302b_ns = cg.esphome_ns.namespace("fusb302b")
|
||||
FUSB302B = fusb302b_ns.class_("FUSB302B", cg.Component, i2c.I2CDevice)
|
||||
|
||||
# USB PD fixed supply voltages are 5, 9, 12, 15 and 20 V; the highest one that does not exceed the
|
||||
# requested voltage is chosen.
|
||||
validate_voltage = cv.int_range(min=5, max=20)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(FUSB302B),
|
||||
cv.Required(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pullup_pin_schema,
|
||||
cv.Required(CONF_REQUEST_VOLTAGE): validate_voltage,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x22)),
|
||||
cv.only_on_esp32,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN])
|
||||
var = cg.new_Pvariable(config[CONF_ID], interrupt_pin)
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
cg.add(var.set_request_voltage(config[CONF_REQUEST_VOLTAGE]))
|
||||
|
||||
@@ -0,0 +1,415 @@
|
||||
#include "fusb302b.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "fusb302b_registers.h"
|
||||
|
||||
namespace esphome::fusb302b {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "fusb302b");
|
||||
|
||||
// Task notification bits
|
||||
static constexpr uint32_t NOTIFY_IRQ = 1 << 0;
|
||||
static constexpr uint32_t NOTIFY_STOP = 1 << 1;
|
||||
|
||||
static constexpr uint32_t TASK_STACK_SIZE = 4096;
|
||||
static constexpr UBaseType_t TASK_PRIORITY = 18;
|
||||
|
||||
static constexpr uint32_t STARTUP_DELAY_MS = 2000;
|
||||
static constexpr uint32_t CC_SETTLE_MS = 5;
|
||||
static constexpr uint32_t CC_POLL_INTERVAL_MS = 500;
|
||||
static constexpr uint32_t PD_POLL_INTERVAL_MS = 100;
|
||||
// Safety net in case an interrupt edge is missed while attached and idle
|
||||
static constexpr uint32_t IDLE_POLL_INTERVAL_MS = 1000;
|
||||
// Time to wait for the source to send its capabilities on its own after attach
|
||||
static constexpr uint32_t SOURCE_CAP_WAIT_MS = 5000;
|
||||
// Time to wait after each GET_SOURCE_CAP / soft reset before the next recovery step
|
||||
static constexpr uint32_t SOURCE_CAP_RETRY_MS = 2500;
|
||||
// Upper bound on back-to-back interrupt handling while INT_N stays asserted
|
||||
static constexpr uint8_t MAX_IRQ_ROUNDS = 8;
|
||||
// BC_LVL is read this many extra times to confirm a stable measurement
|
||||
static constexpr uint8_t CC_STABLE_READS = 5;
|
||||
static constexpr uint8_t CC_LEVEL_UNSTABLE = 0xFF;
|
||||
|
||||
void IRAM_ATTR FUSB302B::gpio_intr(FUSB302B *arg) {
|
||||
TaskHandle_t handle = arg->task_.get_handle();
|
||||
if (handle == nullptr)
|
||||
return;
|
||||
BaseType_t woken = pdFALSE;
|
||||
xTaskNotifyFromISR(handle, NOTIFY_IRQ, eSetBits, &woken);
|
||||
portYIELD_FROM_ISR(woken);
|
||||
}
|
||||
|
||||
void FUSB302B::task_func(void *arg) { static_cast<FUSB302B *>(arg)->run_task_(); }
|
||||
|
||||
void FUSB302B::setup() {
|
||||
uint8_t device_id = 0;
|
||||
if (!this->read_byte(FUSB_DEVICE_ID, &device_id)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
const uint8_t version = device_id & FUSB_DEVICE_ID_VERSION_MASK;
|
||||
if (version != FUSB_DEVICE_ID_VERSION_A && version != FUSB_DEVICE_ID_VERSION_B) {
|
||||
ESP_LOGE(TAG, "Unknown device ID 0x%02X", device_id);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Device ID 0x%02X (product %u, revision %u)", device_id, (device_id >> 2) & 0x3, device_id & 0x3);
|
||||
if (!this->init_chip_()) {
|
||||
this->mark_failed(LOG_STR("Initialization failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
this->interrupt_pin_->setup();
|
||||
// The task must exist before the interrupt is attached, since the ISR notifies it
|
||||
if (!this->task_.create(FUSB302B::task_func, "fusb302b", TASK_STACK_SIZE, this, TASK_PRIORITY, false)) {
|
||||
this->mark_failed(LOG_STR("Failed to create task"));
|
||||
return;
|
||||
}
|
||||
this->interrupt_pin_->attach_interrupt(&FUSB302B::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
}
|
||||
|
||||
void FUSB302B::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"FUSB302B:\n"
|
||||
" Request Voltage: %u V",
|
||||
this->get_request_voltage());
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
}
|
||||
|
||||
void FUSB302B::on_shutdown() {
|
||||
this->interrupt_pin_->detach_interrupt();
|
||||
// The task parks itself instead of being deleted, so it can never be stopped in the middle of an I2C transfer
|
||||
if (this->task_.is_created())
|
||||
xTaskNotify(this->task_.get_handle(), NOTIFY_STOP, eSetBits);
|
||||
}
|
||||
|
||||
void FUSB302B::run_task_() {
|
||||
// Give the source time to settle after power-up before the first CC measurement
|
||||
vTaskDelay(pdMS_TO_TICKS(STARTUP_DELAY_MS));
|
||||
|
||||
while (true) {
|
||||
uint32_t wait_ms = CC_POLL_INTERVAL_MS;
|
||||
if (this->attached_) {
|
||||
const bool busy = this->waiting_for_source_caps_ || this->retry_request_pending_ || this->check_ams_();
|
||||
wait_ms = busy ? PD_POLL_INTERVAL_MS : IDLE_POLL_INTERVAL_MS;
|
||||
}
|
||||
uint32_t bits = 0;
|
||||
xTaskNotifyWait(0, UINT32_MAX, &bits, pdMS_TO_TICKS(wait_ms));
|
||||
|
||||
if (bits & NOTIFY_STOP) {
|
||||
while (true)
|
||||
vTaskSuspend(nullptr);
|
||||
}
|
||||
|
||||
// INT_N is held low until the interrupt registers are read, so a new event that arrives while it is
|
||||
// already low has no falling edge: keep servicing while the pin stays asserted.
|
||||
if (bits & NOTIFY_IRQ)
|
||||
this->handle_interrupt_();
|
||||
for (uint8_t i = 0; i < MAX_IRQ_ROUNDS && !this->interrupt_pin_->digital_read(); i++)
|
||||
this->handle_interrupt_();
|
||||
|
||||
if (!this->attached_) {
|
||||
this->try_attach_();
|
||||
continue;
|
||||
}
|
||||
this->check_transition_timeout_();
|
||||
this->check_request_retry_();
|
||||
this->check_source_caps_();
|
||||
}
|
||||
}
|
||||
|
||||
bool FUSB302B::init_chip_() {
|
||||
// Software reset restores all registers to their defaults
|
||||
if (!this->write_byte(FUSB_RESET, FUSB_RESET_SW_RES))
|
||||
return false;
|
||||
if (!this->reset_pd_())
|
||||
return false;
|
||||
|
||||
// Unmask the interrupts used for PD messaging and VBUS changes
|
||||
if (!this->write_byte(FUSB_MASK1, FUSB_MASK1_DEFAULT) || !this->write_byte(FUSB_MASKA, 0) ||
|
||||
!this->write_byte(FUSB_MASKB, 0))
|
||||
return false;
|
||||
|
||||
// Let the chip retransmit unacknowledged messages up to 3 times
|
||||
uint8_t control3 = 0;
|
||||
if (!this->read_byte(FUSB_CONTROL3, &control3))
|
||||
return false;
|
||||
control3 =
|
||||
(control3 & ~FUSB_CONTROL3_N_RETRIES_MASK) | (3 << FUSB_CONTROL3_N_RETRIES_SHIFT) | FUSB_CONTROL3_AUTO_RETRY;
|
||||
if (!this->write_byte(FUSB_CONTROL3, control3))
|
||||
return false;
|
||||
|
||||
if (!this->write_byte(FUSB_POWER, FUSB_POWER_ALL))
|
||||
return false;
|
||||
return this->reset_pd_();
|
||||
}
|
||||
|
||||
bool FUSB302B::reset_pd_() {
|
||||
this->reset_protocol_();
|
||||
// Writing CONTROL0 as a whole byte also clears INT_MASK, which is what enables the INT_N pin
|
||||
return this->write_byte(FUSB_CONTROL0, FUSB_CONTROL0_TX_FLUSH) &&
|
||||
this->write_byte(FUSB_CONTROL1, FUSB_CONTROL1_RX_FLUSH) && this->write_byte(FUSB_RESET, FUSB_RESET_PD_RESET);
|
||||
}
|
||||
|
||||
bool FUSB302B::enable_auto_crc_() {
|
||||
uint8_t switches1 = 0;
|
||||
return this->read_byte(FUSB_SWITCHES1, &switches1) &&
|
||||
this->write_byte(FUSB_SWITCHES1, switches1 | FUSB_SWITCHES1_AUTO_CRC);
|
||||
}
|
||||
|
||||
void FUSB302B::handle_interrupt_() {
|
||||
// STATUS0A, STATUS1A, INTERRUPTA, INTERRUPTB, STATUS0, STATUS1, INTERRUPT; reading clears the interrupts
|
||||
uint8_t regs[7];
|
||||
if (this->read_register(FUSB_STATUS0A, regs, sizeof(regs)) != i2c::ERROR_OK) {
|
||||
this->enter_error_();
|
||||
return;
|
||||
}
|
||||
const uint8_t interrupta = regs[2];
|
||||
const uint8_t status0 = regs[4];
|
||||
uint8_t status1 = regs[5];
|
||||
const uint8_t interrupt = regs[6];
|
||||
|
||||
if ((interrupt & FUSB_INTERRUPT_I_VBUSOK) && !(status0 & FUSB_STATUS0_VBUSOK)) {
|
||||
if (this->attached_)
|
||||
this->detach_();
|
||||
return;
|
||||
}
|
||||
if (interrupta & FUSB_INTERRUPTA_I_HARDRST) {
|
||||
if (this->attached_)
|
||||
this->handle_hard_reset_();
|
||||
return;
|
||||
}
|
||||
if (interrupta & FUSB_INTERRUPTA_I_SOFTRST) {
|
||||
ESP_LOGV(TAG, "Soft reset received");
|
||||
}
|
||||
if ((interrupta & FUSB_INTERRUPTA_I_RETRYFAIL) && this->attached_)
|
||||
this->handle_transmit_failed_();
|
||||
|
||||
if (!this->attached_)
|
||||
return;
|
||||
PdMsg msg;
|
||||
while (!(status1 & FUSB_STATUS1_RX_EMPTY)) {
|
||||
bool is_sop = false;
|
||||
if (!this->read_message_(msg, is_sop)) {
|
||||
ESP_LOGW(TAG, "Reading message failed");
|
||||
// A partial read leaves the FIFO misaligned
|
||||
this->write_byte(FUSB_CONTROL1, FUSB_CONTROL1_RX_FLUSH);
|
||||
return;
|
||||
}
|
||||
// Only SOP packets are addressed to a sink; SOP'/SOP'' are for cable plugs
|
||||
if (is_sop)
|
||||
this->handle_message_(msg);
|
||||
if (!this->read_byte(FUSB_STATUS1, &status1))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool FUSB302B::read_message_(PdMsg &msg, bool &is_sop) {
|
||||
uint8_t token = 0;
|
||||
uint8_t header[2];
|
||||
if (!this->read_byte(FUSB_FIFOS, &token) || this->read_register(FUSB_FIFOS, header, 2) != i2c::ERROR_OK)
|
||||
return false;
|
||||
msg.set_header(encode_uint16(header[1], header[0]));
|
||||
|
||||
// Data objects, followed by the CRC32 that the chip already checked
|
||||
uint8_t buf[PD_MAX_NUM_DATA_OBJECTS * 4 + 4];
|
||||
if (this->read_register(FUSB_FIFOS, buf, msg.num_of_obj * 4 + 4) != i2c::ERROR_OK)
|
||||
return false;
|
||||
for (uint8_t i = 0; i < msg.num_of_obj; i++) {
|
||||
const uint8_t *obj = &buf[i * 4];
|
||||
msg.data_objects[i] = encode_uint32(obj[3], obj[2], obj[1], obj[0]);
|
||||
}
|
||||
is_sop = (token & FUSB_FIFO_RX_TOKEN_BITS) == FUSB_FIFO_RX_SOP;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FUSB302B::send_message(const PdMsg &msg) {
|
||||
// Register address, then the TX tokens; written in one transfer without the copy write_register() makes
|
||||
uint8_t buf[1 + 4 + 1 + 2 + PD_MAX_NUM_DATA_OBJECTS * 4 + 4];
|
||||
size_t len = 0;
|
||||
const uint16_t header = msg.get_coded_header();
|
||||
|
||||
buf[len++] = FUSB_FIFOS;
|
||||
buf[len++] = FUSB_TX_TOKEN_SYNC1;
|
||||
buf[len++] = FUSB_TX_TOKEN_SYNC1;
|
||||
buf[len++] = FUSB_TX_TOKEN_SYNC1;
|
||||
buf[len++] = FUSB_TX_TOKEN_SYNC2;
|
||||
buf[len++] = FUSB_TX_TOKEN_PACKSYM | (msg.num_of_obj * 4 + 2);
|
||||
buf[len++] = header & 0xFF;
|
||||
buf[len++] = header >> 8;
|
||||
for (uint8_t i = 0; i < msg.num_of_obj; i++) {
|
||||
const uint32_t obj = msg.data_objects[i];
|
||||
buf[len++] = obj & 0xFF;
|
||||
buf[len++] = (obj >> 8) & 0xFF;
|
||||
buf[len++] = (obj >> 16) & 0xFF;
|
||||
buf[len++] = obj >> 24;
|
||||
}
|
||||
buf[len++] = FUSB_TX_TOKEN_JAM_CRC;
|
||||
buf[len++] = FUSB_TX_TOKEN_EOP;
|
||||
buf[len++] = FUSB_TX_TOKEN_TXOFF;
|
||||
buf[len++] = FUSB_TX_TOKEN_TXON;
|
||||
|
||||
i2c::ErrorCode err = this->write(buf, len);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Sending message type %u failed (error %d)", msg.type, static_cast<int>(err));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FUSB302B::measure_cc_(uint8_t meas_switch, uint8_t &level) {
|
||||
if (!this->write_byte(FUSB_SWITCHES0, FUSB_SWITCHES0_PDWN_1 | FUSB_SWITCHES0_PDWN_2 | meas_switch))
|
||||
return false;
|
||||
vTaskDelay(pdMS_TO_TICKS(CC_SETTLE_MS));
|
||||
uint8_t status0 = 0;
|
||||
if (!this->read_byte(FUSB_STATUS0, &status0))
|
||||
return false;
|
||||
level = status0 & FUSB_STATUS0_BC_LVL;
|
||||
for (uint8_t i = 0; i < CC_STABLE_READS; i++) {
|
||||
if (!this->read_byte(FUSB_STATUS0, &status0))
|
||||
return false;
|
||||
if ((status0 & FUSB_STATUS0_BC_LVL) != level) {
|
||||
level = CC_LEVEL_UNSTABLE;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void FUSB302B::try_attach_() {
|
||||
if (this->state_ == PdState::PD_STATE_ERROR) {
|
||||
if (!this->init_chip_())
|
||||
return;
|
||||
ESP_LOGI(TAG, "Communication restored");
|
||||
this->set_state_(PdState::PD_STATE_DISCONNECTED);
|
||||
}
|
||||
|
||||
uint8_t status0 = 0;
|
||||
if (!this->write_byte(FUSB_POWER, FUSB_POWER_ALL) || !this->read_byte(FUSB_STATUS0, &status0)) {
|
||||
this->enter_error_();
|
||||
return;
|
||||
}
|
||||
// A source may present its CC pull-up before VBUS is on; detach is only detected when VBUS drops
|
||||
if (!(status0 & FUSB_STATUS0_VBUSOK)) {
|
||||
this->cc_ambiguous_logged_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t cc1 = 0;
|
||||
uint8_t cc2 = 0;
|
||||
if (!this->write_byte(FUSB_SWITCHES1, FUSB_SWITCHES1_SPECREV_REV2) ||
|
||||
!this->write_byte(FUSB_MEASURE, FUSB_MEASURE_MDAC_CC) || !this->measure_cc_(FUSB_SWITCHES0_MEAS_CC1, cc1) ||
|
||||
!this->measure_cc_(FUSB_SWITCHES0_MEAS_CC2, cc2)) {
|
||||
this->enter_error_();
|
||||
return;
|
||||
}
|
||||
// Exactly one CC line carries the source pull-up; the other one is open
|
||||
uint8_t meas_switch;
|
||||
uint8_t tx_switch;
|
||||
if (cc1 != CC_LEVEL_UNSTABLE && cc1 > 0 && cc2 == 0) {
|
||||
meas_switch = FUSB_SWITCHES0_MEAS_CC1;
|
||||
tx_switch = FUSB_SWITCHES1_TXCC1;
|
||||
} else if (cc2 != CC_LEVEL_UNSTABLE && cc2 > 0 && cc1 == 0) {
|
||||
meas_switch = FUSB_SWITCHES0_MEAS_CC2;
|
||||
tx_switch = FUSB_SWITCHES1_TXCC2;
|
||||
} else {
|
||||
if (!this->cc_ambiguous_logged_) {
|
||||
ESP_LOGD(TAG, "VBUS present but no single CC line with a source pull-up (CC1 %u, CC2 %u)", cc1, cc2);
|
||||
this->cc_ambiguous_logged_ = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->write_byte(FUSB_SWITCHES0, FUSB_SWITCHES0_PDWN_1 | FUSB_SWITCHES0_PDWN_2 | meas_switch) ||
|
||||
!this->write_byte(FUSB_SWITCHES1, tx_switch | FUSB_SWITCHES1_SPECREV_REV2) || !this->enable_auto_crc_() ||
|
||||
!this->reset_pd_()) {
|
||||
this->enter_error_();
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Attached on CC%u", meas_switch == FUSB_SWITCHES0_MEAS_CC1 ? 1 : 2);
|
||||
this->attached_ = true;
|
||||
this->cc_ambiguous_logged_ = false;
|
||||
this->start_source_cap_wait_(SourceCapStep::SOURCE_CAP_STEP_WAIT);
|
||||
this->set_state_and_contract_(PdState::PD_STATE_DEFAULT_CONTRACT, DEFAULT_CONTRACT);
|
||||
}
|
||||
|
||||
void FUSB302B::detach_() {
|
||||
ESP_LOGD(TAG, "Detached (VBUS lost)");
|
||||
this->attached_ = false;
|
||||
this->waiting_for_source_caps_ = false;
|
||||
this->reset_pd_();
|
||||
this->clear_contract_();
|
||||
this->set_state_(PdState::PD_STATE_DISCONNECTED);
|
||||
}
|
||||
|
||||
void FUSB302B::handle_hard_reset_() {
|
||||
ESP_LOGD(TAG, "Hard reset received");
|
||||
// The source returns to Type-C default power and sends its capabilities again
|
||||
// Also flushes the RX FIFO, so the caller has nothing left to read
|
||||
if (!this->reset_pd_()) {
|
||||
this->enter_error_();
|
||||
return;
|
||||
}
|
||||
this->set_state_and_contract_(PdState::PD_STATE_DEFAULT_CONTRACT, DEFAULT_CONTRACT);
|
||||
this->start_source_cap_wait_(SourceCapStep::SOURCE_CAP_STEP_WAIT);
|
||||
}
|
||||
|
||||
void FUSB302B::enter_error_() {
|
||||
if (this->state_ == PdState::PD_STATE_ERROR)
|
||||
return;
|
||||
ESP_LOGW(TAG, "Communication with the chip failed");
|
||||
this->attached_ = false;
|
||||
this->waiting_for_source_caps_ = false;
|
||||
this->clear_contract_();
|
||||
this->set_state_(PdState::PD_STATE_ERROR);
|
||||
}
|
||||
|
||||
void FUSB302B::send_soft_reset_() {
|
||||
this->reset_pd_();
|
||||
this->send_message(this->make_control_msg_(PD_CNTRL_SOFT_RESET));
|
||||
// The source answers a soft reset with new source capabilities
|
||||
this->start_source_cap_wait_(SourceCapStep::SOURCE_CAP_STEP_SOFT_RESET_SENT);
|
||||
}
|
||||
|
||||
void FUSB302B::start_source_cap_wait_(SourceCapStep step) {
|
||||
this->waiting_for_source_caps_ = true;
|
||||
this->source_cap_step_ = step;
|
||||
this->source_cap_step_start_ = millis();
|
||||
}
|
||||
|
||||
void FUSB302B::check_source_caps_() {
|
||||
if (!this->waiting_for_source_caps_ || this->check_ams_())
|
||||
return;
|
||||
const uint32_t elapsed = millis() - this->source_cap_step_start_;
|
||||
switch (this->source_cap_step_) {
|
||||
case SourceCapStep::SOURCE_CAP_STEP_WAIT:
|
||||
if (elapsed < SOURCE_CAP_WAIT_MS)
|
||||
return;
|
||||
ESP_LOGD(TAG, "No source capabilities received, requesting them");
|
||||
this->send_message(this->make_control_msg_(PD_CNTRL_GET_SOURCE_CAP));
|
||||
this->source_cap_step_ = SourceCapStep::SOURCE_CAP_STEP_GET_SENT;
|
||||
break;
|
||||
case SourceCapStep::SOURCE_CAP_STEP_GET_SENT:
|
||||
if (elapsed < SOURCE_CAP_RETRY_MS)
|
||||
return;
|
||||
ESP_LOGD(TAG, "No source capabilities received, sending soft reset");
|
||||
this->send_soft_reset_();
|
||||
return;
|
||||
case SourceCapStep::SOURCE_CAP_STEP_SOFT_RESET_SENT:
|
||||
if (elapsed < SOURCE_CAP_RETRY_MS)
|
||||
return;
|
||||
ESP_LOGW(TAG, "PD negotiation failed, staying at 5 V");
|
||||
this->waiting_for_source_caps_ = false;
|
||||
this->set_state_(PdState::PD_STATE_PD_TIMEOUT);
|
||||
return;
|
||||
}
|
||||
this->source_cap_step_start_ = millis();
|
||||
}
|
||||
|
||||
} // namespace esphome::fusb302b
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/static_task.h"
|
||||
|
||||
#include "pd.h"
|
||||
|
||||
namespace esphome::fusb302b {
|
||||
|
||||
/// FUSB302B USB Type-C / Power Delivery sink controller.
|
||||
///
|
||||
/// A dedicated task owns all I2C traffic to the chip so that PD replies meet the protocol timing
|
||||
/// even while the main loop is busy.
|
||||
class FUSB302B : public PowerDelivery, public Component, public i2c::I2CDevice {
|
||||
public:
|
||||
explicit FUSB302B(InternalGPIOPin *interrupt_pin) : interrupt_pin_(interrupt_pin) {}
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void on_shutdown() override;
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
protected:
|
||||
enum class SourceCapStep : uint8_t {
|
||||
SOURCE_CAP_STEP_WAIT,
|
||||
SOURCE_CAP_STEP_GET_SENT,
|
||||
SOURCE_CAP_STEP_SOFT_RESET_SENT,
|
||||
};
|
||||
|
||||
static void gpio_intr(FUSB302B *arg);
|
||||
static void task_func(void *arg);
|
||||
|
||||
bool send_message(const PdMsg &msg) override;
|
||||
void on_state_changed() override {}
|
||||
void on_soft_reset_received() override { this->start_source_cap_wait_(SourceCapStep::SOURCE_CAP_STEP_WAIT); }
|
||||
void on_source_caps_received() override { this->waiting_for_source_caps_ = false; }
|
||||
void on_transition_timeout() override { this->send_soft_reset_(); }
|
||||
|
||||
// PHY task only
|
||||
[[noreturn]] void run_task_();
|
||||
bool init_chip_();
|
||||
bool reset_pd_();
|
||||
bool enable_auto_crc_();
|
||||
void handle_interrupt_();
|
||||
bool read_message_(PdMsg &msg, bool &is_sop);
|
||||
void try_attach_();
|
||||
bool measure_cc_(uint8_t meas_switch, uint8_t &level);
|
||||
void detach_();
|
||||
void handle_hard_reset_();
|
||||
void start_source_cap_wait_(SourceCapStep step);
|
||||
void send_soft_reset_();
|
||||
void check_source_caps_();
|
||||
void enter_error_();
|
||||
|
||||
InternalGPIOPin *interrupt_pin_;
|
||||
StaticTask task_;
|
||||
|
||||
// PHY task only
|
||||
uint32_t source_cap_step_start_{0};
|
||||
SourceCapStep source_cap_step_{SourceCapStep::SOURCE_CAP_STEP_WAIT};
|
||||
bool attached_{false};
|
||||
bool waiting_for_source_caps_{false};
|
||||
bool cc_ambiguous_logged_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::fusb302b
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::fusb302b {
|
||||
|
||||
// Register addresses
|
||||
static constexpr uint8_t FUSB_DEVICE_ID = 0x01;
|
||||
static constexpr uint8_t FUSB_SWITCHES0 = 0x02;
|
||||
static constexpr uint8_t FUSB_SWITCHES1 = 0x03;
|
||||
static constexpr uint8_t FUSB_MEASURE = 0x04;
|
||||
static constexpr uint8_t FUSB_CONTROL0 = 0x06;
|
||||
static constexpr uint8_t FUSB_CONTROL1 = 0x07;
|
||||
static constexpr uint8_t FUSB_CONTROL3 = 0x09;
|
||||
static constexpr uint8_t FUSB_MASK1 = 0x0A;
|
||||
static constexpr uint8_t FUSB_POWER = 0x0B;
|
||||
static constexpr uint8_t FUSB_RESET = 0x0C;
|
||||
static constexpr uint8_t FUSB_MASKA = 0x0E;
|
||||
static constexpr uint8_t FUSB_MASKB = 0x0F;
|
||||
static constexpr uint8_t FUSB_STATUS0A = 0x3C;
|
||||
static constexpr uint8_t FUSB_STATUS0 = 0x40;
|
||||
static constexpr uint8_t FUSB_STATUS1 = 0x41;
|
||||
static constexpr uint8_t FUSB_FIFOS = 0x43;
|
||||
|
||||
// DEVICE_ID: version in bits 7:4, product variant (I2C address 0x22-0x25) in bits 3:2, silicon revision in bits 1:0
|
||||
static constexpr uint8_t FUSB_DEVICE_ID_VERSION_MASK = 0xF0;
|
||||
static constexpr uint8_t FUSB_DEVICE_ID_VERSION_A = 0x80;
|
||||
static constexpr uint8_t FUSB_DEVICE_ID_VERSION_B = 0x90;
|
||||
|
||||
// SWITCHES0 bits
|
||||
static constexpr uint8_t FUSB_SWITCHES0_MEAS_CC2 = 1 << 3;
|
||||
static constexpr uint8_t FUSB_SWITCHES0_MEAS_CC1 = 1 << 2;
|
||||
static constexpr uint8_t FUSB_SWITCHES0_PDWN_2 = 1 << 1;
|
||||
static constexpr uint8_t FUSB_SWITCHES0_PDWN_1 = 1 << 0;
|
||||
|
||||
// SWITCHES1 bits
|
||||
static constexpr uint8_t FUSB_SWITCHES1_SPECREV_REV2 = 0x01 << 5;
|
||||
static constexpr uint8_t FUSB_SWITCHES1_AUTO_CRC = 1 << 2;
|
||||
static constexpr uint8_t FUSB_SWITCHES1_TXCC2 = 1 << 1;
|
||||
static constexpr uint8_t FUSB_SWITCHES1_TXCC1 = 1 << 0;
|
||||
|
||||
// MEASURE value used for CC detection
|
||||
static constexpr uint8_t FUSB_MEASURE_MDAC_CC = 49;
|
||||
|
||||
// CONTROL0 bits
|
||||
static constexpr uint8_t FUSB_CONTROL0_TX_FLUSH = 1 << 6;
|
||||
|
||||
// CONTROL1 bits
|
||||
static constexpr uint8_t FUSB_CONTROL1_RX_FLUSH = 1 << 2;
|
||||
|
||||
// CONTROL3 bits
|
||||
static constexpr uint8_t FUSB_CONTROL3_N_RETRIES_SHIFT = 1;
|
||||
static constexpr uint8_t FUSB_CONTROL3_N_RETRIES_MASK = 0x3 << FUSB_CONTROL3_N_RETRIES_SHIFT;
|
||||
static constexpr uint8_t FUSB_CONTROL3_AUTO_RETRY = 1 << 0;
|
||||
|
||||
// MASK1: mask the ACTIVITY, CRC_CHK and BC_LVL interrupts
|
||||
static constexpr uint8_t FUSB_MASK1_DEFAULT = 0x51;
|
||||
|
||||
// POWER bits
|
||||
static constexpr uint8_t FUSB_POWER_ALL = 0x0F;
|
||||
|
||||
// RESET bits
|
||||
static constexpr uint8_t FUSB_RESET_PD_RESET = 1 << 1;
|
||||
static constexpr uint8_t FUSB_RESET_SW_RES = 1 << 0;
|
||||
|
||||
// INTERRUPTA bits
|
||||
static constexpr uint8_t FUSB_INTERRUPTA_I_RETRYFAIL = 1 << 4;
|
||||
static constexpr uint8_t FUSB_INTERRUPTA_I_SOFTRST = 1 << 1;
|
||||
static constexpr uint8_t FUSB_INTERRUPTA_I_HARDRST = 1 << 0;
|
||||
|
||||
// INTERRUPT bits
|
||||
static constexpr uint8_t FUSB_INTERRUPT_I_VBUSOK = 1 << 7;
|
||||
|
||||
// STATUS0 bits
|
||||
static constexpr uint8_t FUSB_STATUS0_VBUSOK = 1 << 7;
|
||||
static constexpr uint8_t FUSB_STATUS0_BC_LVL = 0x03;
|
||||
|
||||
// STATUS1 bits
|
||||
static constexpr uint8_t FUSB_STATUS1_RX_EMPTY = 1 << 5;
|
||||
|
||||
// RX FIFO token
|
||||
static constexpr uint8_t FUSB_FIFO_RX_TOKEN_BITS = 0xE0;
|
||||
static constexpr uint8_t FUSB_FIFO_RX_SOP = 0xE0;
|
||||
|
||||
// TX FIFO tokens
|
||||
static constexpr uint8_t FUSB_TX_TOKEN_TXON = 0xA1;
|
||||
static constexpr uint8_t FUSB_TX_TOKEN_SYNC1 = 0x12;
|
||||
static constexpr uint8_t FUSB_TX_TOKEN_SYNC2 = 0x13;
|
||||
static constexpr uint8_t FUSB_TX_TOKEN_PACKSYM = 0x80;
|
||||
static constexpr uint8_t FUSB_TX_TOKEN_JAM_CRC = 0xFF;
|
||||
static constexpr uint8_t FUSB_TX_TOKEN_EOP = 0x14;
|
||||
static constexpr uint8_t FUSB_TX_TOKEN_TXOFF = 0xFE;
|
||||
|
||||
} // namespace esphome::fusb302b
|
||||
@@ -30,6 +30,7 @@ enum PdControlMsgType : uint8_t {
|
||||
PD_CNTRL_REJECT = 0x04,
|
||||
PD_CNTRL_PING = 0x05,
|
||||
PD_CNTRL_PS_RDY = 0x06,
|
||||
PD_CNTRL_GET_SOURCE_CAP = 0x07,
|
||||
PD_CNTRL_GET_SINK_CAP = 0x08,
|
||||
PD_CNTRL_WAIT = 0x0C,
|
||||
PD_CNTRL_SOFT_RESET = 0x0D,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
fusb302b:
|
||||
id: pd
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
request_voltage: 20
|
||||
@@ -0,0 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
fusb302b: !include common.yaml
|
||||
Reference in New Issue
Block a user