blob: fb00e29476af7094ca605a98dcc0d976540f4272 [file]
/* Copyright 2024 The ChromiumOS Authors
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
/*
* PD Controller subsystem
*/
#include "zephyr/device.h"
#include "zephyr/toolchain.h"
#define DT_DRV_COMPAT named_usbc_port
#include "battery.h"
#include "battery_smart.h"
#include "charge_manager.h"
#include "chipset.h"
#include "drivers/ucsi_v3.h"
#include "ec_commands.h"
#include "hooks.h"
#include "power_button.h"
#include "test/util.h"
#include "timer.h"
#include "usb_common.h"
#include "usb_mux.h"
#include "usb_pd.h"
#include "usbc/pdc_dpm.h"
#include "usbc/pdc_power_mgmt.h"
#include <string.h>
#include <zephyr/devicetree.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/smf.h>
#include <zephyr/sys/atomic.h>
#include <zephyr/sys/clock.h>
#ifdef CONFIG_ZTEST
#include <zephyr/ztest.h>
#endif
#include <drivers/pdc.h>
#include <usbc/utils.h>
LOG_MODULE_REGISTER(pdc_power_mgmt, CONFIG_USB_PDC_LOG_LEVEL);
#ifdef CONFIG_TEST_SNIFF_POWER_MGMT_PDC_APIS
/* Faking PDC APIs directly causes compilation errors of the function being
* redefined. For testing only create a wrapper function that can be faked.
* These are used to sniff function arguments like in dead battery testing
* we are able to FAKE these functions in the test to check the arguments to
* validate we are not changing the RDO when more than one sink path is enabled
* as its happening during driver initialization.
* As these are intended to be FAKED in a test, we won't be able to get coverage
* on these functions.
*/
/* LCOV_EXCL_START */
test_mockable_static_inline int
sniff_pdc_set_sink_path(const struct device *dev, bool en)
{
return pdc_set_sink_path(dev, en);
}
#define pdc_set_sink_path sniff_pdc_set_sink_path
test_mockable_static_inline int sniff_pdc_set_rdo(const struct device *dev,
uint32_t rdo)
{
return pdc_set_rdo(dev, rdo);
}
#define pdc_set_rdo sniff_pdc_set_rdo
/* LCOV_EXCL_STOP */
#endif /* CONFIG_ZTEST */
/**
* @brief Event triggered by sending an internal command
*/
#define PDC_SM_EVENT BIT(0)
/**
* @brief Event triggered when a public command has completed
*/
#define PDC_PUBLIC_CMD_COMPLETE_EVENT BIT(1)
/**
* @brief Event triggered when pdc state has settled
*/
#define PDC_SM_SETTLED_EVENT BIT(2)
/**
* @brief Event triggered when get_connector_status from pdc has completed
*/
#define PDC_PPM_CONNECTOR_STATUS_READY BIT(3)
/**
* @brief Time delay before running the state machine loop
*/
#define LOOP_DELAY_MS 25
/**
* @brief Time delay to wait for a public command to complete
*/
#define PUBLIC_CMD_DELAY_MS 10
/**
* @brief Maximum time to wait for a command to complete.
*/
#define PDC_CMD_TIMEOUT_MS 2000
/**
* @brief Maximum time to wait for a contract to be established after sending
* the SET_RDO command when entering the sink state.
*
* This value is set empirically based on a typical settling time (500-600ms)
* with a generous amount of extra time in case the PDC is slow to report the
* new RDO. Not knowing the true RDO could lead to incorrectly seeding
* charge_manager, so allow a lot of leeway. Most cases will pass through this
* state quickly. After this timeout, assume the RDO is not changing (for any
* reason, but likely external factors) and seed charge_manager based on the
* currently reported RDO, not what we attempted to set.
*/
#define NEW_CONTRACT_TIMEOUT K_MSEC(3000)
/**
* @brief Time to wait for typec only devices (Non PD) to settle
*/
#define TYPEC_ONLY_SINK_DEBOUNCE_TIME_US (1000 * USEC_PER_MSEC)
/**
* @brief maximum number of times to try and send a command, or wait for a
* public API command to execute (Time is 2s)
*
*/
#define WAIT_MAX (PDC_CMD_TIMEOUT_MS / LOOP_DELAY_MS)
/**
* @brief Maximum time to wait for PDC state to settle.
*/
#define PDC_SM_SETTLED_TIMEOUT_MS \
CONFIG_PDC_POWER_MGMT_STATE_MACHINE_SETTLED_TIMEOUT_MS
/** @brief Delay to wait for stable power state before running hooks */
#define PDC_POWER_STATE_DEBOUNCE_MS \
(K_MSEC(CONFIG_PDC_POWER_MGMT_POWER_STATE_DEBOUNCE_PERIOD_MS))
/**
* @brief maximum number of times to try and send a command, or wait for a
* public API command to execute (Time is 2s)
*
*/
#define CMD_RESEND_MAX 2
/**
* @brief maximum number of VDOs
*/
#define VDO_NUM 8
/**
* @brief Minimum long button press in seconds.
*/
#define PD_POWER_BUTTON_LONG_PRESS 4
/**
* @brief Button press timeout in seconds.
*/
#define PD_POWER_BUTTON_PRESS_TIMEOUT 8
/**
* @brief PDC driver commands
*/
enum pdc_cmd_t {
/** CMD_PDC_NONE */
CMD_PDC_NONE,
/** CMD_PDC_RESET */
CMD_PDC_RESET,
/** CMD_PDC_SET_POWER_LEVEL */
CMD_PDC_SET_POWER_LEVEL,
/** CMD_PDC_SET_CCOM */
CMD_PDC_SET_CCOM,
/** CMD_PDC_SET_DRP */
CMD_PDC_SET_DRP,
/** CMD_PDC_GET_DRP */
CMD_PDC_GET_DRP,
/** CMD_PDC_GET_PDOS */
CMD_PDC_GET_PDOS,
/** CMD_PDC_GET_RDO */
CMD_PDC_GET_RDO,
/** CMD_PDC_SET_RDO */
CMD_PDC_SET_RDO,
/** CMD_PDC_SET_SINK_PATH */
CMD_PDC_SET_SINK_PATH,
/** CMD_PDC_READ_POWER_LEVEL */
CMD_PDC_READ_POWER_LEVEL,
/** CMD_PDC_GET_INFO */
CMD_PDC_GET_INFO,
/** CMD_PDC_GET_CONNECTOR_CAPABILITY */
CMD_PDC_GET_CONNECTOR_CAPABILITY,
/** CMD_PDC_SET_UOR */
CMD_PDC_SET_UOR,
/** CMD_PDC_SET_PDR */
CMD_PDC_SET_PDR,
/** CMD_PDC_GET_CONNECTOR_STATUS */
CMD_PDC_GET_CONNECTOR_STATUS,
/** CMD_PDC_GET_CABLE_PROPERTY */
CMD_PDC_GET_CABLE_PROPERTY,
/** CMD_PDC_GET_VDO */
CMD_PDC_GET_VDO,
/** CMD_PDC_CONNECTOR_RESET */
CMD_PDC_CONNECTOR_RESET,
/** CMD_PDC_GET_IDENTITY_DISCOVERY */
CMD_PDC_GET_IDENTITY_DISCOVERY,
/** CMD_PDC_IS_SOURCING_VCONN */
CMD_PDC_IS_VCONN_SOURCING,
/** CMD_PDC_GET_PD_VDO_DP_CFG */
CMD_PDC_GET_PD_VDO_DP_CFG_SELF,
/** CMD_PDC_SET_PDOS */
CMD_PDC_SET_PDOS,
/** CMD_PDC_GET_PCH_DATA_STATUS */
CMD_PDC_GET_PCH_DATA_STATUS,
/** CMD_PDC_ACK_CC_CI */
CMD_PDC_ACK_CC_CI,
/** CMD_PDC_GET_LPM_PPM_INFO */
CMD_PDC_GET_LPM_PPM_INFO,
/** CMD_PDC_SET_FRS */
CMD_PDC_SET_FRS,
/** CMD_PDC_GET_ATTENTION_VDO */
CMD_PDC_GET_ATTENTION_VDO,
/** CMD_PDC_GET_SBU_MUX_MODE */
CMD_PDC_GET_SBU_MUX_MODE,
/** CMD_PDC_SET_SBU_MUX_MODE */
CMD_PDC_SET_SBU_MUX_MODE,
/** CMD_PDC_SET_AP_POWER_STATE */
CMD_PDC_SET_AP_POWER_STATE,
/** CMD_PDC_SET_BBR_CTS */
CMD_PDC_SET_BBR_CTS,
/** CMD_PDC_SET_BATTERY_STATUS */
CMD_PDC_SET_BATTERY_STATUS,
/** CMD_PDC_SET_BATTERY_CAPABILITY*/
CMD_PDC_SET_BATTERY_CAPABILITY,
/** CMD_PDC_GET_VENDOR_STATUS */
CMD_PDC_GET_VENDOR_STATUS,
/** CMD_PDC_GET_ALERT */
CMD_PDC_GET_ALERT,
/** CMD_PDC_SET_MAX_PDP */
CMD_PDC_SET_MAX_PDP,
/** CMD_PDC_COUNT */
CMD_PDC_COUNT
};
/**
* @brief Send Local States
*/
enum send_cmd_state_t {
/** SEND_CMD_START_ENTRY */
SEND_CMD_START_ENTRY,
/** SEND_CMD_START_RUN */
SEND_CMD_START_RUN,
/** SEND_CMD_WAIT_ENTRY */
SEND_CMD_WAIT_ENTRY,
/** SEND_CMD_WAIT_RUN */
SEND_CMD_WAIT_RUN,
/** SEND_CMD_WAIT_EXIT */
SEND_CMD_WAIT_EXIT,
};
/**
* @brief Command type
*/
struct cmd_t {
/** Command to send */
enum pdc_cmd_t cmd;
/** True if command is pending */
bool pending;
/** != 0 if command failed to send */
int8_t error;
};
/**
* @brief Send command type
*/
struct send_cmd_t {
/** Send command local state */
enum send_cmd_state_t local_state;
/* Wait counter used in local wait state */
uint16_t wait_counter;
/* Command resend counter */
uint8_t resend_counter;
/* Command sent from public API */
struct cmd_t public;
/* Command sent from internal API */
struct cmd_t intern;
};
/**
* @brief SNK Attached Local States
*/
enum snk_attached_local_state_t {
/** SNK_ATTACHED_GET_CONNECTOR_CAPABILITY */
SNK_ATTACHED_GET_CONNECTOR_CAPABILITY,
/** SNK_ATTACHED_SET_DR_SWAP_POLICY */
SNK_ATTACHED_SET_DR_SWAP_POLICY,
/** SNK_ATTACHED_SET_PR_SWAP_POLICY */
SNK_ATTACHED_SET_PR_SWAP_POLICY,
/** SNK_ATTACHED_GET_PDOS */
SNK_ATTACHED_GET_PDOS,
/** SNK_ATTACHED_GET_VDO */
SNK_ATTACHED_GET_VDO,
/** SNK_ATTACHED_WAIT_FOR_CONTRACT */
SNK_ATTACHED_WAIT_FOR_CONTRACT,
/** SNK_ATTACHED_SYNC_CHARGE_MGR */
SNK_ATTACHED_SYNC_CHARGE_MGR,
/** SNK_ATTACHED_SET_SINK_PATH */
SNK_ATTACHED_SET_SINK_PATH,
/** SNK_ATTACHED_EVALUATE_PDOS */
SNK_ATTACHED_EVALUATE_PDOS,
/** SNK_ATTACHED_GET_SINK_PDO */
SNK_ATTACHED_GET_SINK_PDO,
/** SNK_ATTACHED_GET_CABLE_PROPERTY */
SNK_ATTACHED_GET_CABLE_PROPERTY,
/** SNK_ATTACHED_READ_POWER_LEVEL */
SNK_ATTACHED_READ_POWER_LEVEL,
/** SNK_ATTACHED_RUN */
SNK_ATTACHED_RUN,
/* Mark end of enum */
SNK_ATTACHED_INVALID,
};
#ifdef CONFIG_PDC_POWER_MGMT_LOG_SUBSTATES
/* Names of the Sink-attached substates. */
const static char *snk_attached_local_state_names[] = {
[SNK_ATTACHED_GET_CONNECTOR_CAPABILITY] = "GET_CONN_CAP",
[SNK_ATTACHED_SET_DR_SWAP_POLICY] = "SET_DR_SWAP_POLICY",
[SNK_ATTACHED_SET_PR_SWAP_POLICY] = "SET_PR_SWAP_POLICY",
[SNK_ATTACHED_GET_PDOS] = "GET_PDOS",
[SNK_ATTACHED_GET_VDO] = "GET_VDO",
[SNK_ATTACHED_WAIT_FOR_CONTRACT] = "WAIT_FOR_CNRCT",
[SNK_ATTACHED_SYNC_CHARGE_MGR] = "SYNC_CHARGE_MGR",
[SNK_ATTACHED_SET_SINK_PATH] = "SET_SINK_PATH",
[SNK_ATTACHED_EVALUATE_PDOS] = "EVAL_PDOS",
[SNK_ATTACHED_GET_SINK_PDO] = "GET_SINK_PDO",
[SNK_ATTACHED_GET_CABLE_PROPERTY] = "GET_CABLE_PROP",
[SNK_ATTACHED_READ_POWER_LEVEL] = "RD_PWR_LVL",
[SNK_ATTACHED_RUN] = "RUN",
};
BUILD_ASSERT(ARRAY_SIZE(snk_attached_local_state_names) == SNK_ATTACHED_INVALID,
"Please update substate names array");
#endif /* CONFIG_PDC_POWER_MGMT_LOG_SUBSTATES */
/**
* @brief SRC Attached Local States
*/
enum src_attached_local_state_t {
/** SRC_ATTACHED_SET_SINK_PATH_OFF */
SRC_ATTACHED_SET_SINK_PATH_OFF,
/** SRC_ATTACHED_GET_CONNECTOR_CAPABILITY */
SRC_ATTACHED_GET_CONNECTOR_CAPABILITY,
/** SRC_ATTACHED_SET_DR_SWAP_POLICY */
SRC_ATTACHED_SET_DR_SWAP_POLICY,
/** SRC_ATTACHED_SET_PR_SWAP_POLICY */
SRC_ATTACHED_SET_PR_SWAP_POLICY,
/** SRC_ATTACHED_READ_POWER_LEVEL */
SRC_ATTACHED_READ_POWER_LEVEL,
/** SRC_ATTACHED_GET_VDO */
SRC_ATTACHED_GET_VDO,
/** SRC_ATTACHED_GET_CABLE_PROPERTY */
SRC_ATTACHED_GET_CABLE_PROPERTY,
/** SRC_ATTACHED_RUN */
SRC_ATTACHED_RUN,
/* Mark end of enum */
SRC_ATTACHED_INVALID,
};
#ifdef CONFIG_PDC_POWER_MGMT_LOG_SUBSTATES
/* Names of the Source-attached substates. */
const static char *src_attached_local_state_names[] = {
[SRC_ATTACHED_SET_SINK_PATH_OFF] = "SET_SINK_PATH_OFF",
[SRC_ATTACHED_GET_CONNECTOR_CAPABILITY] = "GET_CONN_CAP",
[SRC_ATTACHED_SET_DR_SWAP_POLICY] = "SET_DR_SWAP_POLICY",
[SRC_ATTACHED_SET_PR_SWAP_POLICY] = "SET_PR_SWAP_POLICY",
[SRC_ATTACHED_READ_POWER_LEVEL] = "RD_PWR_LVL",
[SRC_ATTACHED_GET_VDO] = "GET_VDO",
[SRC_ATTACHED_GET_CABLE_PROPERTY] = "GET_CABLE_PROP",
[SRC_ATTACHED_RUN] = "RUN",
};
BUILD_ASSERT(ARRAY_SIZE(src_attached_local_state_names) == SRC_ATTACHED_INVALID,
"Please update substate names array");
#endif /* CONFIG_PDC_POWER_MGMT_LOG_SUBSTATES */
/**
* @brief TypeC SNK Attached Local States
*/
enum snk_typec_attached_local_state_t {
/** SNK_TYPEC_ATTACHED_SET_CHARGE_CURRENT */
SNK_TYPEC_ATTACHED_SET_CHARGE_CURRENT,
/** SNK_TYPEC_ATTACHED_DEBOUNCE */
SNK_TYPEC_ATTACHED_DEBOUNCE,
/** SNK_TYPEC_READ_POWER_LEVEL */
SNK_TYPEC_READ_POWER_LEVEL,
/** SNK_TYPEC_ATTACHED_RUN */
SNK_TYPEC_ATTACHED_RUN,
/* Mark end of enum */
SNK_TYPEC_ATTACHED_INVALID,
};
#ifdef CONFIG_PDC_POWER_MGMT_LOG_SUBSTATES
/* Names of the Type-C only Sink-attached substates. */
const static char *snk_typec_attached_local_state_names[] = {
[SNK_TYPEC_ATTACHED_SET_CHARGE_CURRENT] = "SET_CHRG_CUR",
[SNK_TYPEC_ATTACHED_DEBOUNCE] = "DEBOUNCE",
[SNK_TYPEC_READ_POWER_LEVEL] = "RD_PWR_LVL",
[SNK_TYPEC_ATTACHED_RUN] = "RUN",
};
BUILD_ASSERT(ARRAY_SIZE(snk_typec_attached_local_state_names) ==
SNK_TYPEC_ATTACHED_INVALID,
"Please update substate names array");
#endif /* CONFIG_PDC_POWER_MGMT_LOG_SUBSTATES */
/**
* @brief TypeC SRC Attached Local States
*/
enum src_typec_attached_local_state_t {
/** SRC_TYPEC_ATTACHED_SET_SINK_PATH_OFF */
SRC_TYPEC_ATTACHED_SET_SINK_PATH_OFF,
/** SRC_TYPEC_ATTACHED_DEBOUNCE */
SRC_TYPEC_ATTACHED_DEBOUNCE,
/** SRC_TYPEC_ATTACHED_ADD_SINK */
SRC_TYPEC_ATTACHED_ADD_SINK,
/** SRC_TYPEC_READ_POWER_LEVEL */
SRC_TYPEC_READ_POWER_LEVEL,
/** SRC_TYPEC_ATTACHED_RUN */
SRC_TYPEC_ATTACHED_RUN,
/* Mark end of enum */
SRC_TYPEC_ATTACHED_INVALID,
};
#ifdef CONFIG_PDC_POWER_MGMT_LOG_SUBSTATES
/* Names of the Type-C only Source-attached substates. */
const static char *src_typec_attached_local_state_names[] = {
[SRC_TYPEC_ATTACHED_SET_SINK_PATH_OFF] = "SET_SINK_PATH_OFF",
[SRC_TYPEC_ATTACHED_DEBOUNCE] = "DEBOUNCE",
[SRC_TYPEC_ATTACHED_ADD_SINK] = "ADD_SINK",
[SRC_TYPEC_READ_POWER_LEVEL] = "RD_PWR_LVL",
[SRC_TYPEC_ATTACHED_RUN] = "RUN",
};
BUILD_ASSERT(ARRAY_SIZE(src_typec_attached_local_state_names) ==
SRC_TYPEC_ATTACHED_INVALID,
"Please update substate names array");
#endif /* CONFIG_PDC_POWER_MGMT_LOG_SUBSTATES */
/**
* @brief Unattached Local States
*/
enum unattached_local_state_t {
/** UNATTACHED_SET_SINK_PATH_OFF */
UNATTACHED_SET_SINK_PATH_OFF,
/** UNATTACHED_RUN */
UNATTACHED_RUN,
/* Mark end of enum */
UNATTACHED_INVALID,
};
#ifdef CONFIG_PDC_POWER_MGMT_LOG_SUBSTATES
/* Names of the Source-attached substates. */
const static char *unattached_local_state_names[] = {
[UNATTACHED_SET_SINK_PATH_OFF] = "SET_SINK_PATH_OFF",
[UNATTACHED_RUN] = "RUN",
};
BUILD_ASSERT(ARRAY_SIZE(unattached_local_state_names) == UNATTACHED_INVALID,
"Please update substate names array");
#endif /* CONFIG_PDC_POWER_MGMT_LOG_SUBSTATES */
/**
* @brief Initialization local states. Carry out these steps upon subsystem
* startup. INIT_GET_CONNECTOR_STATUS must be the final step, as this
* calls the GET_CONNECTOR_STATUS PDC command and will directly
* transition to the appropriate unattached or attached state.
*/
enum init_local_state_t {
/** INIT_WAIT_FOR_READY - Wait until the underlying PDC driver is
* initialized.
*/
INIT_WAIT_FOR_READY,
/** INIT_SET_SBU_MUX_FORCED_DEBUG - set the port's SBU mux to
* forced-debug if the COMMON_POLICY_SET_SBU_MUX_TO_FORCED_DEBUG flag
* is set.
*/
INIT_SET_SBU_MUX_FORCED_DEBUG,
/** INIT_SET_SINK_PDOS - Set the sink PDOs (sink capabilities) on the
* PDC based on product configuration data.
*/
INIT_SET_SINK_PDOS,
/** INIT_SET_SRC_PDOS - pdc sets src pdo in advance during
* initialization.
*/
INIT_SET_SRC_PDOS,
/** INIT_FRS - Enable or disable FRS on the device during
* initialization.
*/
INIT_SET_FRS,
/**
* INIT_SET_MAX_PDP - Set the device's max PDP during init based on
* number of 3A ports.
*/
INIT_SET_MAX_PDP,
/** INIT_GET_CONNECTOR_STATUS - Get current status. This state does not
* return; the state machine will transition to the unattached or one
* of the attached run states after handling the response.
*/
INIT_GET_CONNECTOR_STATUS,
/* Mark end of enum */
INIT_INVALID,
};
#ifdef CONFIG_PDC_POWER_MGMT_LOG_SUBSTATES
/* Names of the init substates. */
const static char *init_local_state_names[] = {
[INIT_WAIT_FOR_READY] = "WAIT_FOR_READY",
[INIT_SET_SINK_PDOS] = "SET_SINK_PDOS",
[INIT_SET_SRC_PDOS] = "SET_SRC_PDOS",
[INIT_SET_FRS] = "SET_FRS",
[INIT_SET_MAX_PDP] = "SET_MAX_PDP",
[INIT_GET_CONNECTOR_STATUS] = "GET_CONN_STATUS",
};
BUILD_ASSERT(ARRAY_SIZE(init_local_state_names) == INIT_INVALID,
"Please update substate names array");
#endif /* CONFIG_PDC_POWER_MGMT_LOG_SUBSTATES */
/**
* @brief CCI Event Flags
*/
enum cci_flag_t {
/** CCI_BUSY */
CCI_BUSY,
/** CCI_ERROR */
CCI_ERROR,
/** CCI_CMD_COMPLETED */
CCI_CMD_COMPLETED,
/** CCI_EVENT: Used to trigger querying connector status */
CCI_EVENT,
/** Used to query vendor defined connector change bits */
CCI_VENDOR_EVENT,
/** CCI_CAM_CHANGE */
CCI_CAM_CHANGE,
/** CCI_ACK */
CCI_ACK,
/** CCI_ATTENTION */
CCI_ATTENTION,
/** CCI_PPM_EVENT */
CCI_PPM_EVENT,
/**
* Trigger sending new SINK PDOs for the LPM. This is not part of
* the UCSI connector change flags, but this action is valid
* during UNATTACHED, SNK_ATTACHED, SRC_ATTACHED, SNK_TYPEC_ONLY,
* and SRC_TYPEC_ONLY
*/
CCI_SET_SINK_PDOS,
/** CCI_FLAGS_COUNT */
CCI_FLAGS_COUNT
};
/**
* @brief PDC Command Names
*/
test_export_static const char *const pdc_cmd_names[] = {
[CMD_PDC_NONE] = "",
[CMD_PDC_RESET] = "PDC_RESET",
[CMD_PDC_SET_POWER_LEVEL] = "PDC_SET_POWER_LEVEL",
[CMD_PDC_SET_CCOM] = "PDC_SET_CCOM",
[CMD_PDC_SET_DRP] = "PDC_SET_DRP",
[CMD_PDC_GET_DRP] = "PDC_GET_DRP",
[CMD_PDC_GET_PDOS] = "PDC_GET_PDOS",
[CMD_PDC_GET_RDO] = "PDC_GET_RDO",
[CMD_PDC_SET_RDO] = "PDC_SET_RDO",
[CMD_PDC_SET_SINK_PATH] = "PDC_SET_SINK_PATH",
[CMD_PDC_READ_POWER_LEVEL] = "PDC_READ_POWER_LEVEL",
[CMD_PDC_GET_INFO] = "PDC_GET_INFO",
[CMD_PDC_GET_CONNECTOR_CAPABILITY] = "PDC_GET_CONNECTOR_CAPABILITY",
[CMD_PDC_SET_UOR] = "PDC_SET_UOR",
[CMD_PDC_SET_PDR] = "PDC_SET_PDR",
[CMD_PDC_GET_CONNECTOR_STATUS] = "PDC_GET_CONNECTOR_STATUS",
[CMD_PDC_GET_CABLE_PROPERTY] = "PDC_GET_CABLE_PROPERTY",
[CMD_PDC_GET_VDO] = "PDC_GET_VDO",
[CMD_PDC_CONNECTOR_RESET] = "PDC_CONNECTOR_RESET",
[CMD_PDC_GET_IDENTITY_DISCOVERY] = "PDC_GET_IDENTITY_DISCOVERY",
[CMD_PDC_IS_VCONN_SOURCING] = "PDC_IS_VCONN_SOURCING",
[CMD_PDC_GET_PD_VDO_DP_CFG_SELF] = "PDC_GET_PD_VDO_DP_CFG_SELF",
[CMD_PDC_SET_PDOS] = "PDC_SET_PDOS",
[CMD_PDC_GET_PCH_DATA_STATUS] = "PDC_GET_PCH_DATA_STATUS",
[CMD_PDC_ACK_CC_CI] = "PDC_ACK_CC_CI",
[CMD_PDC_GET_LPM_PPM_INFO] = "PDC_GET_LPM_PPM_INFO",
[CMD_PDC_SET_FRS] = "PDC_SET_FRS",
[CMD_PDC_GET_ATTENTION_VDO] = "PDC_GET_ATTENTION_VDO",
[CMD_PDC_GET_SBU_MUX_MODE] = "PDC_GET_SBU_MUX_MODE",
[CMD_PDC_SET_SBU_MUX_MODE] = "PDC_SET_SBU_MUX_MODE",
[CMD_PDC_SET_AP_POWER_STATE] = "PDC_SET_AP_POWER_STATE",
[CMD_PDC_SET_BBR_CTS] = "PDC_SET_BBR_CTS",
[CMD_PDC_SET_BATTERY_STATUS] = "PDC_SET_BATTERY_STATUS",
[CMD_PDC_SET_BATTERY_CAPABILITY] = "PDC_SET_BATTERY_CAPABILITY",
[CMD_PDC_GET_VENDOR_STATUS] = "PDC_GET_VENDOR_STATUS",
[CMD_PDC_GET_ALERT] = "PDC_GET_ALERT",
[CMD_PDC_SET_MAX_PDP] = "PDC_SET_MAX_PDP",
};
const int pdc_cmd_types = CMD_PDC_COUNT;
BUILD_ASSERT(ARRAY_SIZE(pdc_cmd_names) == CMD_PDC_COUNT);
/**
* @brief State Machine State Names
*/
static const char *const pdc_state_names[] = {
[PDC_INIT] = "PDC Init",
[PDC_UNATTACHED] = "Unattached",
[PDC_SNK_ATTACHED] = "Attached.SNK",
[PDC_SRC_ATTACHED] = "Attached.SRC",
[PDC_SEND_CMD_START] = "SendCmdStart",
[PDC_SEND_CMD_WAIT] = "SendCmdWait",
[PDC_SRC_TYPEC_ONLY] = "TypeCSrcAttached",
[PDC_SNK_TYPEC_ONLY] = "TypeCSnkAttached",
[PDC_SUSPENDED] = "Suspended",
[PDC_DISABLED] = "Disabled",
[PDC_INVALID] = "PDC Invalid",
};
BUILD_ASSERT(ARRAY_SIZE(pdc_state_names) == PDC_STATE_COUNT,
"pdc_state_names array has wrong number of elements");
/**
* @brief Common policy flags which are applicable regardless of attached state
*/
enum policy_common_t {
/** COMMON_POLICY_SET_POWER_STATE */
COMMON_POLICY_SET_POWER_STATE,
/** COMMON_POLICY_SET_RP */
COMMON_POLICY_SET_RP,
/** COMMON_POLICY_GET_ALERT */
COMMON_POLICY_GET_ALERT,
/** When set, run CMD_PDC_SET_SBU_MUX_MODE to set the port's SBU mux
* mode to force-debug. This should only be set on the port acting
* as the CCD port.
*/
COMMON_POLICY_SET_SBU_MUX_TO_FORCED_DEBUG,
/** COMMON_POLICY_COUNT */
COMMON_POLICY_COUNT,
};
struct pdc_common_policy_t {
/** Common policy flags */
ATOMIC_DEFINE(flags, COMMON_POLICY_COUNT);
/** Current AP power state */
enum power_state ap_state;
};
/**
* @brief Unattached policy flags
*/
enum policy_unattached_t {
/** UNA_POLICY_TCC */
UNA_POLICY_TCC,
/** UNA_POLICY_CC_MODE */
UNA_POLICY_CC_MODE,
/** UNA_POLICY_DRP_MODE */
UNA_POLICY_DRP_MODE,
/** UNA_POLICY_UPDATE_SRC_CAPS */
UNA_POLICY_UPDATE_SRC_CAPS,
/** UNA_POLICY_COUNT */
UNA_POLICY_COUNT,
};
/**
* @brief Unattached policy object
*/
struct pdc_unattached_policy_t {
/** Unattached policy flags */
ATOMIC_DEFINE(flags, UNA_POLICY_COUNT);
/** Type-C current */
enum usb_typec_current_t tcc;
/** CC Operation Mode */
enum ccom_t cc_mode;
/** DRP Mode */
enum drp_mode_t drp_mode;
};
/**
* @brief Sink policy flags
*/
enum policy_snk_attached_t {
/** Request a new power level */
SNK_POLICY_NEW_POWER_REQUEST,
/** New source caps */
SNK_POLICY_NEW_SRC_CAPS_AVAILABLE,
/** Enables swap to Source */
SNK_POLICY_SWAP_TO_SRC,
/** Selects the low power PDO on connect */
SNK_POLICY_REQUEST_LOW_POWER_PDO,
/** Selects the highest powered PDO on connect */
SNK_POLICY_REQUEST_HIGH_POWER_PDO,
/** Selects the active charge port */
SNK_POLICY_SET_ACTIVE_CHARGE_PORT,
/** Runs a test to determine if we should become a source instead */
SNK_POLICY_EVAL_SWAP_TO_SRC,
/** Triggers an update of the allow_pr_swap bit in CMD_SET_DRP */
SNK_POLICY_UPDATE_ALLOW_PR_SWAP,
/** Sends SET_PDO to the LPM. */
SNK_POLICY_UPDATE_SRC_CAPS,
/** Evaluates sink PDOs from DRP partner. */
SNK_POLICY_EVAL_SNK_FIXED_PDO,
/** TypeC sink only */
SNK_POLICY_UPDATE_TYPEC_CURRENT,
/** Update battery status */
SNK_POLICY_UPDATE_BATTERY_STATUS,
/** Update battery capability */
SNK_POLICY_UPDATE_BATTERY_CAPABILITY,
/** SNK_POLICY_COUNT */
SNK_POLICY_COUNT,
};
/**
* @brief Attached state
*/
enum attached_state_t {
/* INIT_STATE */
INIT_STATE,
/** UNATTACHED_STATE */
UNATTACHED_STATE,
/** SRC_ATTACHED_STATE */
SRC_ATTACHED_STATE,
/** SNK_ATTACHED_STATE */
SNK_ATTACHED_STATE,
/** SRC_ATTACHED_TYPEC_ONLY_STATE */
SRC_ATTACHED_TYPEC_ONLY_STATE,
/** SNK_ATTACHED_TYPEC_ONLY_STATE */
SNK_ATTACHED_TYPEC_ONLY_STATE,
};
static const char *const attached_state_names[] = {
[INIT_STATE] = "Init",
[UNATTACHED_STATE] = "Unattached",
[SRC_ATTACHED_STATE] = "Attached.SRC",
[SNK_ATTACHED_STATE] = "Attached.SNK",
[SRC_ATTACHED_TYPEC_ONLY_STATE] = "TypeCSrcAttached",
[SNK_ATTACHED_TYPEC_ONLY_STATE] = "TypeCSnkAttached",
};
/**
* @brief Common struct for PDOs
*/
struct pdc_pdos_t {
/** PDOs */
uint32_t pdos[PDO_MAX_OBJECTS];
/** PDO count */
uint8_t pdo_count;
};
/**
* @brief Struct for SET_PDOS command
*/
struct set_pdos_t {
/** PDOs for SRC or SNK CAPs */
uint32_t pdos[PDO_MAX_OBJECTS];
/** PDO count */
uint8_t count;
/** SRC or SNK pdo */
enum pdo_type_t type;
};
/**
* @brief Sink attached policy object
*/
struct pdc_snk_attached_policy_t {
/** SNK Attached policy flags */
ATOMIC_DEFINE(flags, SNK_POLICY_COUNT);
/** Currently active PDO */
uint32_t pdo;
/** Current active PDO index */
uint32_t pdo_index;
/** PDO count */
uint8_t pdo_count;
/** PDOs for Source Caps */
struct pdc_pdos_t partner_src_pdos;
/** Sent RDO */
uint32_t rdo;
/** New RDO to send */
uint32_t rdo_to_send;
/** If true, accept a power role swap request from port partner */
bool accept_power_role_swap;
};
/**
* @brief Source attached policy flags
*/
enum policy_src_attached_t {
/** Enables swap to Sink */
SRC_POLICY_SWAP_TO_SNK,
/** Forces sink-only operation, even if it requires a disconnect */
SRC_POLICY_FORCE_SNK,
/** Trigger a call into DPM source current balancing policy */
SRC_POLICY_EVAL_SNK_FIXED_PDO,
/** Triggers a Get_Sink_Cap message to the partner. */
SRC_POLICY_GET_SINK_CAPS,
/** Set new SRC CAP for PDC port in source power role */
SRC_POLICY_UPDATE_SRC_CAPS,
/**
* Triggers sending CMD_PDC_GET_RDO to extract RDO for current
* balancing policy.
*/
SRC_POLICY_GET_RDO,
/** Get source caps from DRP sink partner */
SRC_POLICY_GET_SRC_CAPS,
/** Evaluate source PDOs from DRP sink partner */
SRC_POLICY_EVAL_SRC_PDOS,
/** Triggers an update of the allow_pr_swap bit in CMD_SET_DRP */
SRC_POLICY_UPDATE_ALLOW_PR_SWAP,
/** Update battery status */
SRC_POLICY_UPDATE_BATTERY_STATUS,
/** Update battery capability */
SRC_POLICY_UPDATE_BATTERY_CAPABILITY,
/** Hard Reset bus powered device */
SRC_POLICY_TBT_RESET,
/** SRC_POLICY_COUNT */
SRC_POLICY_COUNT
};
/**
* @brief Source attached policy object
*/
struct pdc_src_attached_policy_t {
/** SRC Attached policy flags */
ATOMIC_DEFINE(flags, SRC_POLICY_COUNT);
/** PDOs for Sink caps */
struct pdc_pdos_t partner_snk_pdos;
/** Request RDO from port partner */
uint32_t rdo;
/** Stores our desired LPM source PDO. This is sent to the PDC when the
* {UNA|SNK|SRC}_POLICY_UPDATE_SRC_CAPS policy flags are triggered.
* PDC Power Management chooses this value based on policy.
*/
uint32_t lpm_src_pdo;
/** If true, accept a power role swap request from port partner */
bool accept_power_role_swap;
};
/**
* @brief Indices used to map which VDO to use to extract the desired field
*/
#define IDENTITY_VID_VDO_IDX 0
#define IDENTITY_PTYPE_VDO_IDX 0
#define IDENTITY_PID_VDO_IDX 1
/**
* @brief Invalid value for VDO used to check if VDO has been queried already.
*/
#define INVALID_VDO_VALUE -1u
/**
* @brief Table of VDO types to request in the GET_VDO command
*/
static const enum vdo_type_t vdo_discovery_list[] = {
VDO_ID_HEADER,
VDO_PRODUCT,
};
/**
* @brief PDC Port object
*/
struct pdc_port_t {
/** State machine context */
struct smf_ctx ctx;
/** Subsystem device */
const struct device *dev;
/** PDC device */
const struct device *pdc;
/** CCI flags */
ATOMIC_DEFINE(cci_flags, CCI_FLAGS_COUNT);
/** PDC Cmd flags */
ATOMIC_DEFINE(pdc_cmd_flags, CMD_PDC_COUNT);
/** Flag to suspend the PDC Power Mgmt state machine */
atomic_t suspend;
/** Flag to notify that a Hard Reset was sent */
atomic_t hard_reset_sent;
/** Init local state variable */
enum init_local_state_t init_local_state;
/** Source TypeC attached local state variable */
enum src_typec_attached_local_state_t src_typec_attached_local_state;
/** Sink TypeC attached local state variable */
enum snk_typec_attached_local_state_t snk_typec_attached_local_state;
/** Unattached local state variable */
enum unattached_local_state_t unattached_local_state;
/** Sink attached local state variable */
enum snk_attached_local_state_t snk_attached_local_state;
/** Source attached local state variable */
enum src_attached_local_state_t src_attached_local_state;
/** State machine run event */
struct k_event sm_event;
/** PDC settled event */
struct k_event settle_event;
/** Transitioning from last_state */
enum pdc_state_t last_state;
/* Transitioning to next state */
enum pdc_state_t next_state;
/* Return state from sending a command */
enum pdc_state_t send_cmd_return_state;
/** PDC Unattached policy */
struct pdc_unattached_policy_t una_policy;
/** PDC Sink Attached policy */
struct pdc_snk_attached_policy_t snk_policy;
/** PDC Source Attached policy */
struct pdc_src_attached_policy_t src_policy;
/** PDC Common policy */
struct pdc_common_policy_t common_policy;
/** Cable Property */
union cable_property_t cable_prop;
/** PDC version and other information */
struct pdc_info_t info;
/** Command mutex */
struct k_mutex mtx;
/** PDC command to send */
struct send_cmd_t send_cmd;
/** Pointer to current pending command */
struct cmd_t *cmd;
/** Bit mask of port events; see PD_STATUS_EVENT_* */
atomic_t port_event;
/** CCAPS temp variable used with CMD_PDC_GET_CONNECTOR_CAPABILITY
* command */
union connector_capability_t ccaps;
/** CONNECTOR_STATUS temp variable used with CONNECTOR_GET_STATUS
* command */
union connector_status_t connector_status;
/* Sink path status of the port */
bool sink_path_status;
/** SINK_PATH_EN temp variable used with CMD_PDC_SET_SINK_PATH command
*/
bool sink_path_to_send;
/** Timeout for a new contract to be negotiated after sending SET_RDO
* in the sink entry flow. */
k_timepoint_t new_contract_timeout;
/** Cached VBUS voltage in millivolts */
uint16_t vbus;
/** UOR variable used with CMD_PDC_SET_UOR command */
union uor_t uor;
/** Set the desired power policy, used with CMD_PDC_SET_PDR command */
enum pdc_power_policy pdr_policy;
/** Tracks current connection state */
enum attached_state_t attached_state;
/** GET_VDO temp variable used with CMD_GET_VDO */
union get_vdo_t vdo_req;
/** LPM_PPM_INFO temp variable to hold user buffer pointer */
struct lpm_ppm_info_t *lpm_ppm_info;
/** Array used to hold the list of VDO types to request */
uint8_t vdo_type[VDO_NUM];
/** Array used to store VDOs returned from the GET_VDO command */
uint32_t vdo[VDO_NUM];
/** Store the VDO returned for the PD_VDO_DP_CFG */
uint32_t vdo_dp_cfg;
/** CONNECTOR_RESET temp variable used with CMD_PDC_CONNECTOR_RESET */
union connector_reset_t connector_reset;
/** PD Port Partner discovery state: True if discovery is complete, else
* false */
bool discovery_state;
/** Charge current while in TypeC Sink state */
uint32_t typec_current_ma;
/** Buffer used by public api to receive data from the driver */
uint8_t *public_api_buff;
/** Timer to used to verify typec_only vs USB-PD port partner */
struct k_timer typec_only_timer;
/** Type of PDOs to get: SNK|SRC from PDC or Port Partner */
struct get_pdo_t get_pdo;
/** Variable used to store/set PDC LPM SRC CAPs */
struct set_pdos_t set_pdos;
/** Buffer used by public api to receive data from the driver */
uint8_t pch_data_status[5];
/** SET_DRP variable used with CMD_SET_DRP */
enum drp_mode_t drp;
enum drp_mode_t drp_read;
/** Used by CMD_PDC_SET_SBU_MUX_MODE / CMD_PDC_GET_SBU_MUX_MODE */
enum pdc_sbu_mux_mode sbu_mux_mode;
/** Callback */
struct pdc_callback cc_cb;
struct pdc_callback ci_cb;
/** Callback for PPM */
const struct pdc_callback *ppm_ci_cb;
/** Last configured dual role power state */
enum pd_dual_role_states dual_role_state;
/** Change indicator bits to clear */
union conn_status_change_bits_t ci;
/** Command complete clear bit */
bool cc;
/** Vendor defined change indicator bits */
uint16_t vendor_defined_ci;
/** System should watch for an HPD wake */
bool hpd_wake_watch;
/** Additional change bits to report to PPM. */
union conn_status_change_bits_t overlay_ppm_changes;
/** non-UCSI status change reported by PDC */
union vendor_status_change_bits_t vendor_status_change;
/** LPM should enable FRS. */
bool frs_enable;
/** Store response to the GET_ATTENTION_VDO command */
union get_attention_vdo_t attention_vdo;
/** board callback for Type-C port unattach event */
pdc_power_mgmt_board_unattached_cb board_unattach_cb;
/** board callback for DP Attention event */
pdc_power_mgmt_board_dp_attention_cb board_dp_attention_cb;
/** CMD_SET_BBR_CTS temp variable to communicate the desired state */
bool bbr_cts_enable;
/** Battery status */
union battery_status_t bstat;
/** Battery capability */
union battery_capability_t bcap;
/* Store state of PD power button */
k_timepoint_t pb_long_press;
k_timepoint_t pb_press_timeout;
/* Alert Data Object */
uint32_t ado;
};
/**
* @brief Subsystem PDC Data
*/
struct pdc_data_t {
/** This port's thread */
k_tid_t thread;
/** This port thread's data */
struct k_thread thread_data;
/** Port data */
struct pdc_port_t port;
};
/**
* @brief Subsystem PDC Config
*/
struct pdc_config_t {
/** Port number for the connector */
uint8_t connector_num;
/**
* The usbc stack initializes this pointer that creates the
* main thread for this port
*/
void (*create_thread)(const struct device *dev);
};
/* Source PDO(s) */
#if defined(CONFIG_PDC_POWER_MGMT_SRC_PDO_PEAK_OCP_100)
#define PDO_PEAK_OCP PDO_PEAK_OVERCURR_100
#elif defined(CONFIG_PDC_POWER_MGMT_SRC_PDO_PEAK_OCP_110)
#define PDO_PEAK_OCP PDO_PEAK_OVERCURR_110
#elif defined(CONFIG_PDC_POWER_MGMT_SRC_PDO_PEAK_OCP_125)
#define PDO_PEAK_OCP PDO_PEAK_OVERCURR_125
#elif defined(CONFIG_PDC_POWER_MGMT_SRC_PDO_PEAK_OCP_150)
#define PDO_PEAK_OCP PDO_PEAK_OVERCURR_150
#else
#error Invalid peak overcurrent setting
#endif
static const uint32_t pdo_src_fixed_flags =
(PDO_FIXED_DUAL_ROLE | PDO_FIXED_DATA_SWAP | PDO_FIXED_COMM_CAP |
PDO_FIXED_PEAK_CURR(PDO_PEAK_OCP));
static const uint32_t pdc_src_pdo_nominal =
PDO_FIXED(5000, 1500, pdo_src_fixed_flags);
static const uint32_t pdc_src_pdo_max =
PDO_FIXED(5000, 3000, pdo_src_fixed_flags);
/* Sink PDO(s) */
enum snk_pdo {
SNK_PDO_FIXED_POS,
SNK_PDO_BATT_POS,
SNK_PDO_VAR_POS,
SNK_PDO_COUNT,
};
#define PDO_SINK_FIXED_FLAGS \
(PDO_FIXED_DUAL_ROLE | PDO_FIXED_DATA_SWAP | PDO_FIXED_COMM_CAP)
/* Battery PDO covering 5V-5% to the board maximum voltage and current */
#define SNK_PDO_BATT_DEFAULT \
PDO_BATT(4750, CONFIG_PLATFORM_EC_USB_PD_MAX_VOLTAGE_MV, \
CONFIG_PLATFORM_EC_USB_PD_OPERATING_POWER_MW)
/* Variable PDO covering 5V-5% to the board maximum voltage and current */
#define SNK_PDO_VAR_DEFAULT \
PDO_VAR(4750, CONFIG_PLATFORM_EC_USB_PD_MAX_VOLTAGE_MV, \
CONFIG_PLATFORM_EC_USB_PD_MAX_CURRENT_MA)
struct pdc_pdos_t pdc_snk_pdos = {
.pdos = {
/* Mandatory fixed 5V PDO */
[SNK_PDO_FIXED_POS] = PDO_FIXED(
5000,
MIN((CONFIG_PLATFORM_EC_USB_PD_OPERATING_POWER_MW / 5),
CONFIG_PLATFORM_EC_USB_PD_MAX_CURRENT_MA),
PDO_SINK_FIXED_FLAGS),
[SNK_PDO_BATT_POS] = SNK_PDO_BATT_DEFAULT,
[SNK_PDO_VAR_POS] = SNK_PDO_VAR_DEFAULT,
},
.pdo_count = SNK_PDO_COUNT,
};
static const struct smf_state pdc_states[];
static enum pdc_state_t get_pdc_state(struct pdc_port_t *port);
static void set_pdc_state(struct pdc_port_t *port, enum pdc_state_t next_state);
static int pdc_subsys_init(const struct device *dev);
static void send_cmd_init(struct pdc_port_t *port);
static void queue_internal_cmd(struct pdc_port_t *port, enum pdc_cmd_t pdc_cmd);
static int queue_public_cmd(struct pdc_port_t *port, enum pdc_cmd_t pdc_cmd);
static void init_port_variables(struct pdc_port_t *port,
bool reset_charge_manager);
static void pd_chipset_startup(void);
static void pd_chipset_resume(void);
static void pd_chipset_suspend(void);
static void pd_chipset_shutdown(void);
static void pdc_update_battery_status(struct pdc_port_t *port, bool force);
static void pdc_update_battery_capability(struct pdc_port_t *port);
static void pdc_print_pdo_info(int port, struct pdc_pdos_t *pdo);
static bool should_suspend(struct pdc_port_t *port)
{
if (!atomic_get(&port->suspend)) {
return false;
}
/* Suspend has been requested. Wait until we are in a safe state. */
enum pdc_state_t current_state = get_pdc_state(port);
switch (current_state) {
/* Safe states to suspend from */
case PDC_UNATTACHED:
case PDC_SNK_ATTACHED:
case PDC_SRC_ATTACHED:
case PDC_SNK_TYPEC_ONLY:
case PDC_SRC_TYPEC_ONLY:
return true;
/* Allow suspend from the init state if an error has occurred. This
* allows suspending when the the PDC is stuck in a bootloader mode
* and GET_CONNECTOR_STATUS is repeatedly failing. */
case PDC_INIT:
return (port->cmd->error != 0);
/* Wait for operation to finish. */
case PDC_SEND_CMD_START:
case PDC_SEND_CMD_WAIT:
return false;
/* No need to transition */
case PDC_SUSPENDED:
case PDC_DISABLED:
return false;
case PDC_INVALID:
__ASSERT(0,
"current_state is an unreachable state (PDC_INVALID)");
break;
case PDC_STATE_COUNT:
__ASSERT(0, "Invalid state");
}
__builtin_unreachable();
}
/**
* @brief PDC thread
*/
static ALWAYS_INLINE void pdc_thread(void *pdc_dev, void *unused1,
void *unused2)
{
const struct device *dev = (const struct device *)pdc_dev;
struct pdc_data_t *data = dev->data;
struct pdc_port_t *port = &data->port;
int rv;
while (1) {
/* Wait for timeout or event */
rv = k_event_wait(&port->sm_event, PDC_SM_EVENT, false,
K_MSEC(LOOP_DELAY_MS));
/*
* If k_event_wait returns a non-zero value, then
* always clear PDC_SM_EVENT to ensure that the thread goes to
* sleep in cases where PDC_SM_EVENT can't be handled
* immediately such as when a public cmd is posted, but is
* waiting on an internal cmd to be sent.
*/
if (rv != 0) {
k_event_clear(&port->sm_event, PDC_SM_EVENT);
}
if (should_suspend(port)) {
set_pdc_state(port, PDC_SUSPENDED);
}
/* Run port connection state machine */
smf_run_state(&port->ctx);
}
}
#define PDC_SUBSYS_INIT(inst) \
K_THREAD_STACK_DEFINE(my_stack_area_##inst, \
CONFIG_PDC_POWER_MGMT_STACK_SIZE); \
\
static void create_thread_##inst(const struct device *dev) \
{ \
struct pdc_data_t *data = dev->data; \
\
data->thread = k_thread_create( \
&data->thread_data, my_stack_area_##inst, \
K_THREAD_STACK_SIZEOF(my_stack_area_##inst), \
pdc_thread, (void *)dev, 0, 0, \
CONFIG_PDC_POWER_MGMT_THREAD_PRIORTY, K_ESSENTIAL, \
K_FOREVER); \
k_thread_name_set(data->thread, \
"PDC Power Mgmt" STRINGIFY(inst)); \
} \
\
static struct pdc_data_t data_##inst = { \
.port.dev = DEVICE_DT_INST_GET(inst), /* Initial policy read \
from device tree */ \
.port.pdc = COND_CODE_1( \
CONFIG_PDC_RUNTIME_PORT_CONFIG, (NULL), \
DEVICE_DT_GET(DT_INST_PROP_BY_IDX(inst, pdc, 0))), \
.port.una_policy.tcc = DT_STRING_TOKEN( \
DT_INST_PROP(inst, policy), unattached_rp_value), \
.port.una_policy.cc_mode = DT_STRING_TOKEN( \
DT_INST_PROP(inst, policy), unattached_cc_mode), \
.port.una_policy.drp_mode = DT_STRING_TOKEN( \
DT_INST_PROP(inst, policy), unattached_try), \
.port.suspend = ATOMIC_INIT(0), \
.port.dual_role_state = PD_DRP_TOGGLE_ON, \
}; \
\
static struct pdc_config_t config_##inst = { \
.connector_num = USBC_PORT_NEW(DT_DRV_INST(inst)), \
.create_thread = create_thread_##inst, \
}; \
\
DEVICE_DT_INST_DEFINE(inst, &pdc_subsys_init, NULL, &data_##inst, \
&config_##inst, POST_KERNEL, \
CONFIG_PDC_POWER_MGMT_INIT_PRIORITY, NULL);
DT_INST_FOREACH_STATUS_OKAY(PDC_SUBSYS_INIT)
#ifndef CONFIG_TEST_NO_PDC_INIT
#define PDC_DEVICE_INIT_ONE(node_id) device_init(DEVICE_DT_GET(node_id));
/* Explicitly initialized all ports defined with zephyr,deferred-init */
static int pdc_device_init(void)
{
DT_FOREACH_STATUS_OKAY(DT_DRV_COMPAT, PDC_DEVICE_INIT_ONE);
return 0;
}
SYS_INIT(pdc_device_init, POST_KERNEL, CONFIG_PDC_POWER_MGMT_INIT_PRIORITY);
#endif
/* Enforce initialization order constraints. This driver depends on the PDC
* driver(s) and also charge manager (the latter is enforced by
* `common/charge_manager.c`)
*/
BUILD_ASSERT(CONFIG_PDC_POWER_MGMT_INIT_PRIORITY >
CONFIG_PDC_DRIVER_INIT_PRIORITY,
"pdc_power_mgmt must init after PDC drivers");
#define PDC_DATA_INIT(inst) [USBC_PORT_NEW(DT_DRV_INST(inst))] = &data_##inst,
/**
* @brief data structure used by public API to map port number to PDC_DATA.
* The port number is used to index the array.
*/
static struct pdc_data_t *pdc_data[] = { DT_INST_FOREACH_STATUS_OKAY(
PDC_DATA_INIT) };
/**
* @brief As a sink, this is the max voltage (in millivolts) we can request
* before getting source caps
*/
static uint32_t pdc_max_request_mv = CONFIG_PLATFORM_EC_USB_PD_MAX_VOLTAGE_MV;
/**
* @brief As a sink, this is the max power (in milliwatts) needed to operate
*/
static uint32_t pdc_max_operating_power =
CONFIG_PLATFORM_EC_USB_PD_MAX_POWER_MW;
static enum pdc_state_t get_pdc_state(struct pdc_port_t *port)
{
return port->ctx.current - &pdc_states[0];
}
static void set_pdc_state(struct pdc_port_t *port, enum pdc_state_t next_state)
{
if (get_pdc_state(port) != next_state) {
port->last_state = get_pdc_state(port);
port->next_state = next_state;
smf_set_state(SMF_CTX(port), &pdc_states[next_state]);
}
}
#define SAFE_SUBSTATE_NAME(substate_idx, str_array) \
(IN_RANGE((substate_idx), 0, (ARRAY_SIZE((str_array)) - 1)) ? \
(str_array)[(substate_idx)] : \
"Invalid")
#define PRINT_STATE_WITH_SUBSTATE(port_num, state_idx, substate_idx, \
substate_strs) \
COND_CODE_1(IS_ENABLED(CONFIG_PDC_POWER_MGMT_LOG_SUBSTATES), \
(LOG_INF("C%d State: %s.%s", (port_num), \
pdc_state_names[(state_idx)], \
SAFE_SUBSTATE_NAME((substate_idx), \
(substate_strs)))), \
(LOG_INF("C%d State: %s (%d)", (port_num), \
pdc_state_names[(state_idx)], (substate_idx))))
#define PRINT_STATE(port_num, state_idx) \
LOG_INF("C%d State: %s", (port_num), pdc_state_names[(state_idx)])
static void set_attached_pdc_state(struct pdc_port_t *port,
enum attached_state_t attached_state)
{
const struct pdc_config_t *const config = port->dev->config;
if (attached_state != port->attached_state) {
port->attached_state = attached_state;
LOG_INF("C%d attached: %s", config->connector_num,
attached_state_names[port->attached_state]);
}
}
static void send_cmd_init(struct pdc_port_t *port)
{
port->send_cmd.public.cmd = CMD_PDC_NONE;
port->send_cmd.public.error = 0;
port->send_cmd.public.pending = false;
port->send_cmd.intern.cmd = CMD_PDC_NONE;
port->send_cmd.intern.error = 0;
port->send_cmd.intern.pending = false;
port->send_cmd.local_state = SEND_CMD_START_ENTRY;
}
/**
* @brief Run a command started by a public api function call
*/
static void send_pending_public_commands(struct pdc_port_t *port)
{
/* If we are running public commands, policy state machine must have
* finished settling. Post if not already set.
*/
if (k_event_test(&port->settle_event, PDC_SM_SETTLED_EVENT) == 0) {
k_event_post(&port->settle_event, PDC_SM_SETTLED_EVENT);
}
/* Send a pending public command */
if (port->send_cmd.public.pending) {
set_pdc_state(port, PDC_SEND_CMD_START);
}
}
uint32_t pdc_power_mgmt_get_dp_status(int port)
{
return pdc_data[port]->port.attention_vdo.vdo;
}
atomic_val_t pdc_power_mgmt_get_events(int port)
{
return pdc_data[port]->port.port_event;
}
void pdc_power_mgmt_notify_event(int port, atomic_t event_mask)
{
atomic_or(&pdc_data[port]->port.port_event, event_mask);
pd_send_host_event(PD_EVENT_TYPEC);
}
void pdc_power_mgmt_clear_event(int port, atomic_t event_mask)
{
atomic_and(&pdc_data[port]->port.port_event, ~event_mask);
}
/**
* @brief Limits the charge current to zero and invalidates and received Source
* PDOS. This function also seeds the charger.
*
* @param port PDC port instance
* @param reset_charger If true, reset the charge manager. Should be false
* during system initialization and true otherwise. System initialization
* should bypass charge manager reset so that the charger manager is seeded
* only after the PDC power management thread initializes the PDC state.
*/
static void invalidate_charger_settings(struct pdc_port_t *port,
bool reset_charge_manager)
{
const struct pdc_config_t *const config = port->dev->config;
if (reset_charge_manager) {
typec_set_input_current_limit(config->connector_num, 0, 0);
pd_set_input_current_limit(config->connector_num, 0, 0);
charge_manager_set_ceil(config->connector_num,
CEIL_REQUESTOR_PD, CHARGE_CEIL_NONE);
charge_manager_update_dualrole(config->connector_num,
CAP_UNKNOWN);
}
/* Invalidate PDOS */
port->snk_policy.pdo = 0;
memset(port->snk_policy.partner_src_pdos.pdos, 0,
sizeof(port->snk_policy.partner_src_pdos.pdos));
port->snk_policy.partner_src_pdos.pdo_count = 0;
memset(port->src_policy.partner_snk_pdos.pdos, 0,
sizeof(port->src_policy.partner_snk_pdos.pdos));
port->src_policy.partner_snk_pdos.pdo_count = 0;
}
/**
* @brief Callers of this function should return immediately because the PDC
* state is changed.
*/
static int queue_public_cmd(struct pdc_port_t *port, enum pdc_cmd_t pdc_cmd)
{
/* Don't send if still in init state */
enum pdc_state_t s = get_pdc_state(port);
if (s == PDC_INIT || s == PDC_SUSPENDED || s == PDC_DISABLED) {
return -ENOTCONN;
}
/* Don't send another public initiated command if one is already pending
*/
if (port->send_cmd.public.pending) {
return -EBUSY;
}
k_mutex_lock(&port->mtx, K_FOREVER);
port->send_cmd.public.cmd = pdc_cmd;
port->send_cmd.public.error = 0;
port->send_cmd.public.pending = true;
k_mutex_unlock(&port->mtx);
k_event_post(&port->sm_event, PDC_SM_EVENT);
return 0;
}
/**
* @brief Callers of this function should return immediately because the PDC
* state is changed.
*/
static void queue_internal_cmd(struct pdc_port_t *port, enum pdc_cmd_t pdc_cmd)
{
k_mutex_lock(&port->mtx, K_FOREVER);
port->send_cmd.intern.cmd = pdc_cmd;
port->send_cmd.intern.error = 0;
port->send_cmd.intern.pending = true;
k_mutex_unlock(&port->mtx);
k_event_post(&port->sm_event, PDC_SM_EVENT);
set_pdc_state(port, PDC_SEND_CMD_START);
}
/**
* @brief Trigger a PPM change indication on a port.
*/
static void trigger_ppm_ci(struct pdc_port_t *port)
{
const struct pdc_config_t *config = port->dev->config;
int port_number = config->connector_num;
union cci_event_t cci_event;
if (!port->ppm_ci_cb)
return;
cci_event.connector_change = port_number + 1;
port->ppm_ci_cb->handler(port->dev, port->ppm_ci_cb, cci_event);
}
/**
* @brief Reads connector status and takes appropriate action.
*
* This function should only be called after the completion of the
* GET_CONNECTOR_STATUS command. It reads the connect_status,
* power_operation_mode, and power_direction bit to determine which state should
* be entered.
*
* Note: The caller should return after this call.
*/
static void handle_connector_status(struct pdc_port_t *port)
{
union connector_status_t *status = &port->connector_status;
const struct pdc_config_t *config = port->dev->config;
int port_number = config->connector_num;
union conn_status_change_bits_t conn_status_change_bits;
conn_status_change_bits.raw_value = status->raw_conn_status_change_bits;
LOG_INF("C%d: Connector Change: 0x%04x, RDO: %d", port_number,
conn_status_change_bits.raw_value, RDO_POS(status->rdo));
if (port->sink_path_status != status->sink_path_status) {
LOG_DBG("C%d: Sink path status change: %d", port_number,
status->sink_path_status);
port->sink_path_status = status->sink_path_status;
}
/*
* Set CCI_ACK flag to trigger sending ACK_CC_CI to clear the connector
* change indicator bits which were just read as part of the connector
* status message.
*/
if (conn_status_change_bits.raw_value) {
port->ci.raw_value = conn_status_change_bits.raw_value;
atomic_set_bit(port->cci_flags, CCI_ACK);
}
/* Trigger PPM CI callback if connector status change was indicated. */
if (atomic_test_and_clear_bit(port->cci_flags, CCI_PPM_EVENT)) {
/* We need to also overlay any conn status change bits because
* they may disappear by the time OPM reads them.
*/
port->overlay_ppm_changes.raw_value |=
conn_status_change_bits.raw_value;
trigger_ppm_ci(port);
k_event_post(&port->settle_event,
PDC_PPM_CONNECTOR_STATUS_READY);
}
if (conn_status_change_bits.pd_reset_complete) {
LOG_INF("C%d: Reset complete indicator", port_number);
pdc_power_mgmt_notify_event(port_number,
PD_STATUS_EVENT_HARD_RESET);
atomic_set(&port->hard_reset_sent, true);
}
if (!status->connect_status) {
/* Port is not connected */
port->vbus = 0;
set_pdc_state(port, PDC_UNATTACHED);
return;
}
/* Update cached VBUS voltage (voltage_scale in 5mV increments) */
port->vbus = status->voltage_reading * status->voltage_scale * 5;
switch (status->power_operation_mode) {
case USB_DEFAULT_OPERATION:
port->typec_current_ma = 500;
break;
case BC_OPERATION:
port->typec_current_ma = 500;
break;
case PD_OPERATION:
port->typec_current_ma = 0;
if (conn_status_change_bits.supported_cam) {
atomic_set_bit(port->cci_flags, CCI_CAM_CHANGE);
LOG_INF("C%d: CAM change", port_number);
}
if (conn_status_change_bits.attention) {
atomic_set_bit(port->cci_flags, CCI_ATTENTION);
LOG_INF("C%d: Attention", port_number);
}
if (conn_status_change_bits.supported_provider_caps &&
port->attached_state == SNK_ATTACHED_STATE &&
port->snk_attached_local_state >= SNK_ATTACHED_GET_PDOS) {
/* Source caps have changed. Set the sink-
* attached state machine back to the get PDO
* substate. This will cause PDOs to be re-
* evaluated and the sink path to get enabled
* again. */
atomic_set_bit(port->snk_policy.flags,
SNK_POLICY_NEW_SRC_CAPS_AVAILABLE);
LOG_INF("C%d: New SRC CAPs available", port_number);
k_event_post(&port->sm_event, PDC_SM_EVENT);
}
if (status->power_direction) {
if (conn_status_change_bits.negotiated_power_level) {
/*
* When we are a source, if the partner sends
* a new Request, the PDC sets the negotiated
* power level change. Request sink caps again.
* This flow is atypical for most sink devices,
* but it is behavior PD testers exercise.
*/
atomic_set_bit(port->src_policy.flags,
SRC_POLICY_GET_SINK_CAPS);
}
/* Port partner is a sink device
*/
set_pdc_state(port, PDC_SRC_ATTACHED);
return;
} else {
/* Port partner is a source
* device */
set_pdc_state(port, PDC_SNK_ATTACHED);
return;
}
break;
case USB_TC_CURRENT_1_5A:
port->typec_current_ma = 1500;
break;
case USB_TC_CURRENT_3A:
port->typec_current_ma = 3000;
break;
case USB_TC_CURRENT_5A:
port->typec_current_ma = 5000;
break;
}
/* TypeC only connection */
if (status->power_direction) {
/* Port partner is a Typec Sink device */
set_pdc_state(port, PDC_SRC_TYPEC_ONLY);
return;
} else {
/* Port partner is a Typec Source device */
if (conn_status_change_bits.pwr_operation_mode) {
atomic_set_bit(port->snk_policy.flags,
SNK_POLICY_UPDATE_TYPEC_CURRENT);
}
set_pdc_state(port, PDC_SNK_TYPEC_ONLY);
return;
}
}
/**
* @brief Reads vendor defined connector status change bits, this should only
* be used to process events which are not supported by UCSI.
*/
static void handle_vendor_status(struct pdc_port_t *port)
{
if (port->vendor_status_change.alert_received) {
atomic_set_bit(port->common_policy.flags,
COMMON_POLICY_GET_ALERT);
k_event_post(&port->sm_event, PDC_SM_EVENT);
}
}
/**
* @brief Trigger connector status change on PPM
*
* The UCSI spec says that certain commands with side-effects (like SET_PDR) do
* not generate status change interrupts if the host was the one that caused the
* change. This can create a state de-sync between the EC and OS so we should
* fake some connector changes for capture these side effects.
*
*/
static void trigger_ppm_status_change(struct pdc_port_t *port)
{
union conn_status_change_bits_t status = { .raw_value = 0 };
/* No status change on command error. */
if (!port->cmd || port->cmd->error) {
return;
}
switch (port->cmd->cmd) {
case CMD_PDC_SET_PDR:
status.pwr_direction = 1;
break;
case CMD_PDC_SET_UOR:
status.connector_partner = 1;
break;
case CMD_PDC_SET_PDOS:
status.supported_provider_caps = 1;
break;
case CMD_PDC_SET_SINK_PATH:
status.sink_path_status_change = 1;
break;
case CMD_PDC_READ_POWER_LEVEL:
status.negotiated_power_level = 1;
break;
/* For all other commands, no need to trigger as there shouldn't be
* side-effects to connector status.
*/
default:
return;
}
/* If trigger CI, we should also refresh the connector status. */
atomic_set_bit(port->cci_flags, CCI_EVENT);
k_event_post(&port->sm_event, PDC_SM_EVENT);
port->overlay_ppm_changes.raw_value |= status.raw_value;
trigger_ppm_ci(port);
}
/**
* @brief This function is used to format the GET_VDO command which is used to
* extract VID, PID, and Product Type values from the port partners Discovery
* Identity response message.
*/
static void discovery_info_init(struct pdc_port_t *port)
{
int i;
port->vdo_req.raw_value = 0;
/* Request VDOs from port partner */
port->vdo_req.vdo_origin = VDO_ORIGIN_SOP;
port->vdo_req.num_vdos = ARRAY_SIZE(vdo_discovery_list);
/* Create the list of VDO types being requested */
for (i = 0; i < ARRAY_SIZE(vdo_discovery_list); i++) {
port->vdo_type[i] = vdo_discovery_list[i];
port->vdo[i] = INVALID_VDO_VALUE;
}
/* Clear the DP Config VDO, which stores the DP pin assignment */
port->vdo_dp_cfg = 0;
/* Clear attention VDO which contains DP status */
memset(&port->attention_vdo, 0, sizeof(port->attention_vdo));
}
/**
* @brief This function gets the correct pointer for pdc_pdos_t struct
*
* These structs are used to store SRC/SNK CAPs PDOs of the partner device.
* The pdo_req must specify PARTNER_PDO and the CAP type.
*/
static struct pdc_pdos_t *get_pdc_pdos_ptr(struct pdc_port_t *port,
struct get_pdo_t *pdo_req)
{
struct pdc_pdos_t *pdc_pdos;
__ASSERT(pdo_req->pdo_source == PARTNER_PDO, "Invalid PDO source: %d",
pdo_req->pdo_source);
if (pdo_req->pdo_type == SINK_PDO) {
pdc_pdos = &port->src_policy.partner_snk_pdos;
} else {
pdc_pdos = &port->snk_policy.partner_src_pdos;
}
return pdc_pdos;
}
static bool run_common_policies(struct pdc_port_t *port)
{
if (atomic_test_and_clear_bit(port->common_policy.flags,
COMMON_POLICY_SET_POWER_STATE)) {
/* Send new AP power state to PDC */
queue_internal_cmd(port, CMD_PDC_SET_AP_POWER_STATE);
return true;
}
if (atomic_test_and_clear_bit(port->common_policy.flags,
COMMON_POLICY_SET_RP)) {
/* Check if Rp value needs to be adjusted */
queue_internal_cmd(port, CMD_PDC_SET_POWER_LEVEL);
return true;
}
if (atomic_test_and_clear_bit(port->common_policy.flags,
COMMON_POLICY_GET_ALERT)) {
/* Read latest ADO */
queue_internal_cmd(port, CMD_PDC_GET_ALERT);
return true;
}
/* Note: COMMON_POLICY_SET_SBU_MUX_TO_FORCED_DEBUG is checked in the
* INIT state. */
return false;
}
static void run_unattached_policies(struct pdc_port_t *port)
{
if (run_common_policies(port)) {
return;
}
if (atomic_test_and_clear_bit(port->una_policy.flags,
UNA_POLICY_DRP_MODE)) {
/* Set DRP current policy */
queue_internal_cmd(port, CMD_PDC_SET_DRP);
return;
} else if (atomic_test_and_clear_bit(port->una_policy.flags,
UNA_POLICY_CC_MODE)) {
/* Set CC PULL Resistor and TrySrc or TrySnk */
queue_internal_cmd(port, CMD_PDC_SET_CCOM);
if (port->una_policy.cc_mode == CCOM_DRP) {
/* Apply DRP mode after setting CCOM_DRP
* SET_CCOM doesn't specify what DRP mode we'll enter
* so we should follow up by setting it ourselves.
*/
atomic_set_bit(port->una_policy.flags,
UNA_POLICY_DRP_MODE);
}
return;
} else if (atomic_test_and_clear_bit(port->una_policy.flags,
UNA_POLICY_TCC)) {
/* Set RP current policy */
queue_internal_cmd(port, CMD_PDC_SET_POWER_LEVEL);
/* Make sure new Rp value is applied */
atomic_set_bit(port->una_policy.flags, UNA_POLICY_CC_MODE);
return;
} else if (atomic_test_and_clear_bit(port->una_policy.flags,
UNA_POLICY_UPDATE_SRC_CAPS)) {
/* Ensure the next time a PD capable SNK connects, we offer
* a safe PDO.
*/
port->set_pdos = (struct set_pdos_t){
.count = 1,
.type = SOURCE_PDO,
.pdos = { port->src_policy.lpm_src_pdo },
};
queue_internal_cmd(port, CMD_PDC_SET_PDOS);
return;
}
send_pending_public_commands(port);
}
static bool should_swap_to_source(struct pdc_port_t *port)
{
const struct pdc_config_t *config = port->dev->config;
int port_num = config->connector_num;
/* If all of the following are true, swap to source:
* a) Source caps were received from the port partner
* b) Port partner supports DRP and does not offer unconstrained power
* c) Port isn't the active charging port.
*/
if (port->snk_policy.partner_src_pdos.pdo_count == 0) {
LOG_INF("C%d: %s: Remain sink because partner has no source caps",
port_num, __func__);
return false;
}
/* Only the fixed 5V PDO at index 0 has the UP and DRP bits set */
uint32_t vsafe_5v_pdo = port->snk_policy.partner_src_pdos.pdos[0];
if (vsafe_5v_pdo & PDO_FIXED_GET_UNCONSTRAINED_PWR ||
!(vsafe_5v_pdo & PDO_FIXED_GET_DRP)) {
LOG_INF("C%d: %s: Remain sink because partner has unconstrained "
"power or is not DRP-capable (vSafe5v PDO=0x%08x, "
"active PDO=0x%08x)",
port_num, __func__, vsafe_5v_pdo, port->snk_policy.pdo);
return false;
}
if (charge_manager_get_active_charge_port() == port_num) {
LOG_INF("C%d: %s: Remain sink because this is the active "
"charging port",
port_num, __func__);
return false;
}
return true;
}
static void handle_attention_vdo(struct pdc_port_t *port)
{
const struct pdc_config_t *config = port->dev->config;
int port_num = config->connector_num;
/* Check for an HPD wake on DP Status. The conditions are...
* a) Device is suspended.
* b) Port is currently using an alternate mode.
* c) Port entered suspend in DP Alt Mode with HPD_LVL low.
* d) Updated DP Status has HPD_LVL high.
*/
if (chipset_in_state(CHIPSET_STATE_ANY_SUSPEND) &&
(port->connector_status.conn_partner_flags &
CONNECTOR_PARTNER_FLAG_ALTERNATE_MODE) &&
port->hpd_wake_watch &&
PD_VDO_DPSTS_HPD_LVL(port->attention_vdo.vdo)) {
host_set_single_event(EC_HOST_EVENT_USB_MUX);
}
if (port->board_dp_attention_cb) {
port->board_dp_attention_cb(port_num, port->attention_vdo.vdo);
}
}
static void handle_alert(struct pdc_port_t *port, uint32_t ado)
{
const struct pdc_config_t *config = port->dev->config;
int port_num = config->connector_num;
enum ado_extended_alert_event_type event_type;
if (pdc_power_mgmt_pd_get_data_role(port_num) != PD_ROLE_DFP ||
!(ado & ADO_EXTENDED_ALERT_EVENT)) {
return;
}
event_type = ado & ADO_EXTENDED_ALERT_EVENT_TYPE;
if (event_type == ADO_POWER_BUTTON_PRESS) {
port->pb_press_timeout = sys_timepoint_calc(
K_SECONDS(PD_POWER_BUTTON_PRESS_TIMEOUT));
port->pb_long_press = sys_timepoint_calc(
K_SECONDS(PD_POWER_BUTTON_LONG_PRESS));
} else if (event_type == ADO_POWER_BUTTON_RELEASE) {
/* If the device is on and a long power button press is received
* shutdown the device. Otherwise, simulate a short power button
* press. Release alerts without a corresponding press are
* treated as short power button presses.
*/
if (!sys_timepoint_expired(port->pb_press_timeout)) {
if (sys_timepoint_expired(port->pb_long_press) &&
(chipset_in_state(CHIPSET_STATE_ANY_SUSPEND) |
chipset_in_state(CHIPSET_STATE_ON))) {
chipset_force_shutdown(CHIPSET_SHUTDOWN_BUTTON);
} else {
pdc_power_mgmt_simulate_power_button_press(500);
}
}
port->pb_press_timeout = sys_timepoint_calc(K_FOREVER);
port->pb_long_press = sys_timepoint_calc(K_FOREVER);
}
}
static void pdc_send_sink_pdos(struct pdc_port_t *port)
{
const struct pdc_config_t *config = port->dev->config;
int port_num = config->connector_num;
/* Set sink PDO(s) that reflects this board's max voltage and current */
port->set_pdos = (struct set_pdos_t){
.type = SINK_PDO,
.count = pdc_snk_pdos.pdo_count,
};
pdc_print_pdo_info(port_num, &pdc_snk_pdos);
memcpy(port->set_pdos.pdos, pdc_snk_pdos.pdos,
pdc_snk_pdos.pdo_count * sizeof(uint32_t));
queue_internal_cmd(port, CMD_PDC_SET_PDOS);
}
static void run_snk_policies(struct pdc_port_t *port)
{
const struct pdc_config_t *config = port->dev->config;
int port_num = config->connector_num;
if (run_common_policies(port)) {
return;
}
if (atomic_test_and_clear_bit(port->snk_policy.flags,
SNK_POLICY_SET_ACTIVE_CHARGE_PORT)) {
port->snk_attached_local_state = SNK_ATTACHED_SET_SINK_PATH;
return;
} else if (atomic_test_and_clear_bit(port->snk_policy.flags,
SNK_POLICY_SWAP_TO_SRC)) {
/* Become a source. Set the external swap policy to current
* (sink) setting so that it remains unchanged if the swap
* fails. */
port->pdr_policy = PDC_POWER_POLICY_SOURCE(
port->snk_policy.accept_power_role_swap);
queue_internal_cmd(port, CMD_PDC_SET_PDR);
return;
} else if (atomic_test_and_clear_bit(port->snk_policy.flags,
SNK_POLICY_NEW_POWER_REQUEST)) {
port->snk_attached_local_state = SNK_ATTACHED_EVALUATE_PDOS;
return;
} else if (atomic_test_and_clear_bit(port->snk_policy.flags,
SNK_POLICY_EVAL_SWAP_TO_SRC)) {
bool should_swap = should_swap_to_source(port);
if (should_swap) {
atomic_set_bit(
pdc_data[port_num]->port.snk_policy.flags,
SNK_POLICY_SWAP_TO_SRC);
}
LOG_INF("C%d: SNK_POLICY_EVAL_SWAP_TO_SRC: Prefer %s role.",
config->connector_num, should_swap ? "source" : "sink");
return;
} else if (atomic_test_and_clear_bit(port->snk_policy.flags,
SNK_POLICY_UPDATE_ALLOW_PR_SWAP)) {
/* Remain a sink but update external swap policy */
port->pdr_policy = PDC_POWER_POLICY_SINK(
port->snk_policy.accept_power_role_swap);
queue_internal_cmd(port, CMD_PDC_SET_PDR);
return;
} else if (atomic_test_and_clear_bit(port->snk_policy.flags,
SNK_POLICY_UPDATE_SRC_CAPS)) {
/* Update the LPM with the correct SRC PDO in case there
* is a power role swap.
*/
port->set_pdos = (struct set_pdos_t){
.count = 1,
.type = SOURCE_PDO,
.pdos = { port->src_policy.lpm_src_pdo },
};
queue_internal_cmd(port, CMD_PDC_SET_PDOS);
return;
} else if (atomic_test_and_clear_bit(port->snk_policy.flags,
SNK_POLICY_UPDATE_BATTERY_STATUS)) {
/* Update the PDC with the correct battery status. */
queue_internal_cmd(port, CMD_PDC_SET_BATTERY_STATUS);
return;
} else if (atomic_test_and_clear_bit(
port->snk_policy.flags,
SNK_POLICY_UPDATE_BATTERY_CAPABILITY)) {
/* Update the PDC with the correct battery capabilities. */
queue_internal_cmd(port, CMD_PDC_SET_BATTERY_CAPABILITY);
return;
}
if (pdc_power_mgmt_get_frs_hw_supported(port_num)) {
/* When FRS is supported, the source current limits need
* rebalancing when a DRP source is attached.
*/
if (atomic_test_and_clear_bit(port->snk_policy.flags,
SNK_POLICY_EVAL_SNK_FIXED_PDO)) {
struct get_pdo_t get_partner_snk_pdo = {
.pdo_source = PARTNER_PDO,
.pdo_type = SINK_PDO,
};
uint32_t sink_fixed_pdo =
get_pdc_pdos_ptr(port, &get_partner_snk_pdo)
->pdos[0];
pdc_dpm_eval_sink_fixed_pdo(port_num, sink_fixed_pdo);
return;
}
}
send_pending_public_commands(port);
}
static void run_typec_snk_policies(struct pdc_port_t *port)
{
const struct pdc_config_t *config = port->dev->config;
/* Note - hard resets specifically not checked for here.
* We don't expect hard resets while connected to a non-PD
* partner.
*/
if (run_common_policies(port)) {
return;
}
if (atomic_test_and_clear_bit(port->snk_policy.flags,
SNK_POLICY_SET_ACTIVE_CHARGE_PORT)) {
/* Check if we are the active charge port */
port->sink_path_to_send =
charge_manager_get_active_charge_port() ==
config->connector_num;
if (port->sink_path_to_send) {
/* Set ACOKREF to 5000 */
charge_manager_set_acokref(5000);
}
queue_internal_cmd(port, CMD_PDC_SET_SINK_PATH);
} else if (atomic_test_and_clear_bit(port->snk_policy.flags,
SNK_POLICY_UPDATE_SRC_CAPS)) {
/* Ensure the next time a PD capable SNK connects, we offer
* a safe PDO.
*/
port->set_pdos = (struct set_pdos_t){
.count = 1,
.type = SOURCE_PDO,
.pdos = { port->src_policy.lpm_src_pdo },
};
queue_internal_cmd(port, CMD_PDC_SET_PDOS);
} else if (atomic_test_and_clear_bit(port->snk_policy.flags,
SNK_POLICY_UPDATE_TYPEC_CURRENT)) {
if (port->sink_path_status) {
charge_manager_set_supplier(config->connector_num,
CHARGE_SUPPLIER_TYPEC);
}
pd_set_input_current_limit(config->connector_num, 0, 0);
typec_set_input_current_limit(config->connector_num,
port->typec_current_ma, 5000);
charge_manager_update_dualrole(config->connector_num,
CAP_DEDICATED);
} else {
send_pending_public_commands(port);
}
}
static void run_src_policies(struct pdc_port_t *port)
{
const struct pdc_config_t *config = port->dev->config;
int port_num = config->connector_num;
if (run_common_policies(port)) {
return;
}
if (atomic_test_and_clear_bit(port->src_policy.flags,
SRC_POLICY_SWAP_TO_SNK)) {
/* Become a sink. Set the external swap policy to current
* (source) setting so that it remains unchanged if the swap
* fails. */
port->pdr_policy = PDC_POWER_POLICY_SINK(
port->src_policy.accept_power_role_swap);
queue_internal_cmd(port, CMD_PDC_SET_PDR);
return;
} else if (atomic_test_and_clear_bit(port->src_policy.flags,
SRC_POLICY_FORCE_SNK)) {
queue_internal_cmd(port, CMD_PDC_SET_CCOM);
return;
} else if (atomic_test_and_clear_bit(port->src_policy.flags,
SRC_POLICY_EVAL_SNK_FIXED_PDO)) {
/* Adjust source current limits if necessary */
pdc_dpm_eval_sink_fixed_pdo(
port_num, port->src_policy.partner_snk_pdos.pdos[0]);
/* If the partner is DRP capable, request source caps */
if (port->src_policy.partner_snk_pdos.pdos[0] &
PDO_FIXED_GET_DRP) {
atomic_set_bit(port->src_policy.flags,
SRC_POLICY_GET_SRC_CAPS);
}
return;
} else if (atomic_test_and_clear_bit(port->src_policy.flags,
SRC_POLICY_GET_SINK_CAPS)) {
/* Request up to 4 pdos to honor USCI 6.5.15 Get PDOs - Number
* of PDOs to return starting from the PDO Offset. The number of
* PDOs to return is the value in this field plus 1.
*/
if (!port->get_pdo.updating) {
port->get_pdo.num_pdos = PDO_MAX_OBJECTS;
port->get_pdo.pdo_offset = PDO_OFFSET_0;
port->get_pdo.updating = true;
}
if (port->get_pdo.num_pdos > UCSI_GET_PDOS_MAX_NUM) {
/* More sink caps needed, rearm this path. */
atomic_set_bit(port->src_policy.flags,
SRC_POLICY_GET_SINK_CAPS);
} else {
/* All sink caps will be known following the next
* queued GET_PDOS operation. Trigger source policy
* evaluation.
*/
atomic_set_bit(port->src_policy.flags,
SRC_POLICY_EVAL_SNK_FIXED_PDO);
port->get_pdo.updating = false;
}
port->get_pdo.pdo_type = SINK_PDO;
port->get_pdo.pdo_source = PARTNER_PDO;
queue_internal_cmd(port, CMD_PDC_GET_PDOS);
return;
} else if (atomic_test_and_clear_bit(port->src_policy.flags,
SRC_POLICY_UPDATE_SRC_CAPS)) {
/* Update the PDC SRC_CAP message */
port->set_pdos = (struct set_pdos_t){
.count = 1,
.type = SOURCE_PDO,
.pdos = { port->src_policy.lpm_src_pdo },
};
queue_internal_cmd(port, CMD_PDC_SET_PDOS);
/*
* After sending new SRC_CAP message, get the RDO from the port
* partner to see if the current limit can be adjusted.
*/
atomic_set_bit(port->src_policy.flags, SRC_POLICY_GET_RDO);
return;
} else if (atomic_test_and_clear_bit(port->src_policy.flags,
SRC_POLICY_GET_RDO)) {
/* Get the RDO from the port partner */
queue_internal_cmd(port, CMD_PDC_GET_RDO);
return;
} else if (IS_ENABLED(CONFIG_USBC_PDC_TBT_SUPPORTED) &&
atomic_test_and_clear_bit(port->src_policy.flags,
SRC_POLICY_TBT_RESET)) {
pdc_dpm_tbt_set_reset_ongoing(port_num);
port->connector_reset.raw_value = 0;
port->connector_reset.reset_type = PD_HARD_RESET;
queue_internal_cmd(port, CMD_PDC_CONNECTOR_RESET);
return;
} else if (atomic_test_and_clear_bit(port->src_policy.flags,
SRC_POLICY_GET_SRC_CAPS)) {
/*
* For evaluating swap to sink, we only request the partner's
* first SRC PDO.
*/
port->get_pdo.num_pdos = 1;
port->get_pdo.pdo_offset = PDO_OFFSET_0;
port->get_pdo.pdo_type = SOURCE_PDO;
port->get_pdo.pdo_source = PARTNER_PDO;
port->get_pdo.updating = false;
atomic_set_bit(port->src_policy.flags,
SRC_POLICY_EVAL_SRC_PDOS);
queue_internal_cmd(port, CMD_PDC_GET_PDOS);
return;
} else if (atomic_test_and_clear_bit(port->src_policy.flags,
SRC_POLICY_EVAL_SRC_PDOS)) {
/* Request a swap to sink if the partner has unconstained power
*/
if (port->snk_policy.partner_src_pdos.pdos[0] &
PDO_FIXED_GET_UNCONSTRAINED_PWR) {
atomic_set_bit(port->src_policy.flags,
SRC_POLICY_SWAP_TO_SNK);
}
return;
} else if (atomic_test_and_clear_bit(port->src_policy.flags,
SRC_POLICY_UPDATE_ALLOW_PR_SWAP)) {
/* Remain a source but update external swap policy */
port->pdr_policy = PDC_POWER_POLICY_SOURCE(
port->src_policy.accept_power_role_swap);
queue_internal_cmd(port, CMD_PDC_SET_PDR);
return;
} else if (atomic_test_and_clear_bit(port->src_policy.flags,
SRC_POLICY_UPDATE_BATTERY_STATUS)) {
/* Update the PDC with the correct battery status. */
queue_internal_cmd(port, CMD_PDC_SET_BATTERY_STATUS);
return;
} else if (atomic_test_and_clear_bit(
port->src_policy.flags,
SRC_POLICY_UPDATE_BATTERY_CAPABILITY)) {
/* Update the PDC with the correct battery capabilities. */
queue_internal_cmd(port, CMD_PDC_SET_BATTERY_CAPABILITY);
return;
}
send_pending_public_commands(port);
}
static void run_typec_src_policies(struct pdc_port_t *port)
{
if (run_common_policies(port)) {
return;
}
if (atomic_test_and_clear_bit(port->src_policy.flags,
SRC_POLICY_FORCE_SNK)) {
queue_internal_cmd(port, CMD_PDC_SET_CCOM);
} else if (atomic_test_and_clear_bit(port->src_policy.flags,
SRC_POLICY_UPDATE_SRC_CAPS)) {
/* Ensure the next time a PD capable SNK connects, we offer
* a safe PDO.
*/
port->set_pdos = (struct set_pdos_t){
.count = 1,
.type = SOURCE_PDO,
.pdos = { port->src_policy.lpm_src_pdo },
};
queue_internal_cmd(port, CMD_PDC_SET_PDOS);
} else {
send_pending_public_commands(port);
}
}
/**
* @brief Entering unattached state
*/
static void pdc_unattached_entry(void *obj)
{
struct pdc_port_t *port = (struct pdc_port_t *)obj;
const struct pdc_config_t *config = port->dev->config;
int port_number = config->connector_num;
PRINT_STATE_WITH_SUBSTATE(port_number, get_pdc_state(port),
port->unattached_local_state,
unattached_local_state_names);
set_attached_pdc_state(port, UNATTACHED_STATE);
port->send_cmd.intern.pending = false;
/* Clear any previously set cable property information */
port->cable_prop.raw_value[0] = 0;
port->cable_prop.raw_value[1] = 0;
/* Ensure VDOs aren't valid from previous connection */
discovery_info_init(port);
/* Clear cached VBUS */
port->vbus = 0;
/* Reset PD button */
port->ado = 0;
port->pb_press_timeout = sys_timepoint_calc(K_FOREVER);
port->pb_long_press = sys_timepoint_calc(K_FOREVER);
if (get_pdc_state(port) != port->send_cmd_return_state) {
invalidate_charger_settings(port, true);
port->unattached_local_state = UNATTACHED_SET_SINK_PATH_OFF;
/* Update source current limit policy */
pdc_dpm_remove_sink(port_number);
pdc_dpm_remove_source(port_number);
/* Clear all events except for disconnect. */
pdc_power_mgmt_clear_event(port_number,
BIT_MASK(PD_STATUS_EVENT_COUNT));
pdc_power_mgmt_notify_event(port_number,
PD_STATUS_EVENT_DISCONNECTED);
if (IS_ENABLED(CONFIG_PDC_POWER_MGMT_USB_MUX)) {
usb_mux_set(port_number, USB_PD_MUX_NONE,
USB_SWITCH_DISCONNECT,
/* port is unattacehd, not meaningful */
POLARITY_CC1);
}
if (port->board_unattach_cb) {
port->board_unattach_cb(port_number);
}
}
}
/**
* @brief Run unattached state
*/
static enum smf_state_result pdc_unattached_run(void *obj)
{
struct pdc_port_t *port = (struct pdc_port_t *)obj;
/* The CCI_EVENT is set to re-query connector status, so check the
* connector status and take the appropriate action.
*/
if (atomic_test_and_clear_bit(port->cci_flags, CCI_EVENT)) {
queue_internal_cmd(port, CMD_PDC_GET_CONNECTOR_STATUS);
return SMF_EVENT_HANDLED;
}
if (atomic_test_and_clear_bit(port->cci_flags, CCI_ACK)) {
queue_internal_cmd(port, CMD_PDC_ACK_CC_CI);
return SMF_EVENT_HANDLED;
}
if (atomic_test_and_clear_bit(port->cci_flags, CCI_SET_SINK_PDOS)) {
const struct pdc_config_t *config = port->dev->config;
LOG_INF("C%d: unattached send sink PDOs",
config->connector_num);
pdc_send_sink_pdos(port);
return SMF_EVENT_HANDLED;
}
switch (port->unattached_local_state) {
case UNATTACHED_SET_SINK_PATH_OFF:
port->sink_path_to_send = false;
port->unattached_local_state = UNATTACHED_RUN;
queue_internal_cmd(port, CMD_PDC_SET_SINK_PATH);
return SMF_EVENT_HANDLED;
case UNATTACHED_RUN:
run_unattached_policies(port);
break;
case UNATTACHED_INVALID:
__builtin_unreachable();
}
return SMF_EVENT_HANDLED;
}
/**
* @brief Entering source attached state
*/
static void pdc_src_attached_entry(void *obj)
{
struct pdc_port_t *port = (struct pdc_port_t *)obj;
const struct pdc_config_t *config = port->dev->config;
int port_number = config->connector_num;
PRINT_STATE_WITH_SUBSTATE(port_number, get_pdc_state(port),
port->src_attached_local_state,
src_attached_local_state_names);
set_attached_pdc_state(port, SRC_ATTACHED_STATE);
port->send_cmd.intern.pending = false;
if (get_pdc_state(port) != port->send_cmd_return_state) {
invalidate_charger_settings(port, true);
port->src_attached_local_state = SRC_ATTACHED_SET_SINK_PATH_OFF;
port->get_pdo = (struct get_pdo_t){ 0 };
/* We always want to evalulate sink caps when we a source. */
atomic_set_bit(port->src_policy.flags,
SRC_POLICY_GET_SINK_CAPS);
if (IS_ENABLED(CONFIG_PDC_POWER_MGMT_USB_MUX)) {
usb_mux_set(
port_number, USB_PD_MUX_USB_ENABLED,
USB_SWITCH_CONNECT,
pdc_power_mgmt_pd_get_polarity(port_number));
}
/* Update the PDC with the correct battery status. */
pdc_update_battery_capability(port);
pdc_update_battery_status(port, true);
}
/* Clear a piece of sink policy as it is no longer relevant in the
* sourcing state.
*/
atomic_clear_bit(port->snk_policy.flags, SNK_POLICY_EVAL_SWAP_TO_SRC);
}
/**
* @brief Run source attached state
*/
static enum smf_state_result pdc_src_attached_run(void *obj)
{
struct pdc_port_t *port = (struct pdc_port_t *)obj;
/* The CCI_EVENT is set to re-query connector status, so check the
* connector status and take the appropriate action.
*/
if (atomic_test_and_clear_bit(port->cci_flags, CCI_EVENT)) {
queue_internal_cmd(port, CMD_PDC_GET_CONNECTOR_STATUS);
return SMF_EVENT_HANDLED;
}
if (atomic_test_and_clear_bit(port->cci_flags, CCI_VENDOR_EVENT)) {
queue_internal_cmd(port, CMD_PDC_GET_VENDOR_STATUS);
return SMF_EVENT_HANDLED;
}
if (atomic_test_and_clear_bit(port->cci_flags, CCI_ACK)) {
queue_internal_cmd(port, CMD_PDC_ACK_CC_CI);
return SMF_EVENT_HANDLED;
}
if (atomic_test_and_clear_bit(port->cci_flags, CCI_CAM_CHANGE)) {
queue_internal_cmd(port, CMD_PDC_GET_PD_VDO_DP_CFG_SELF);
return SMF_EVENT_HANDLED;
}
if (atomic_test_and_clear_bit(port->cci_flags, CCI_ATTENTION)) {
queue_internal_cmd(port, CMD_PDC_GET_ATTENTION_VDO);
return SMF_EVENT_HANDLED;
}
if (atomic_test_and_clear_bit(port->cci_flags, CCI_SET_SINK_PDOS)) {
const struct pdc_config_t *config = port->dev->config;
LOG_INF("C%d: SRC attached send sink PDOs",
config->connector_num);
pdc_send_sink_pdos(port);
return SMF_EVENT_HANDLED;
}
/* TODO: b/319643480 - Brox: implement SRC policies */
switch (port->src_attached_local_state) {
case SRC_ATTACHED_SET_SINK_PATH_OFF:
port->sink_path_to_send = false;
port->src_attached_local_state =
SRC_ATTACHED_GET_CONNECTOR_CAPABILITY;
queue_internal_cmd(port, CMD_PDC_SET_SINK_PATH);
return SMF_EVENT_HANDLED;
case SRC_ATTACHED_GET_CONNECTOR_CAPABILITY:
port->src_attached_local_state =
SRC_ATTACHED_SET_DR_SWAP_POLICY;
queue_internal_cmd(port, CMD_PDC_GET_CONNECTOR_CAPABILITY);
return SMF_EVENT_HANDLED;
case SRC_ATTACHED_SET_DR_SWAP_POLICY:
port->src_attached_local_state =
SRC_ATTACHED_SET_PR_SWAP_POLICY;
/* TODO read from DT */
port->uor.swap_to_dfp = 1;
port->uor.swap_to_ufp = 0;
port->uor.accept_dr_swap = 0;
queue_internal_cmd(port, CMD_PDC_SET_UOR);
return SMF_EVENT_HANDLED;
case SRC_ATTACHED_SET_PR_SWAP_POLICY:
port->src_attached_local_state = SRC_ATTACHED_READ_POWER_LEVEL;
/* TODO: read from DT */
port->pdr_policy = PDC_POWER_POLICY_SOURCE(
port->src_policy.accept_power_role_swap);
queue_internal_cmd(port, CMD_PDC_SET_PDR);
atomic_clear_bit(port->src_policy.flags,
SRC_POLICY_UPDATE_ALLOW_PR_SWAP);
return SMF_EVENT_HANDLED;
case SRC_ATTACHED_READ_POWER_LEVEL:
port->src_attached_local_state = SRC_ATTACHED_GET_VDO;
queue_internal_cmd(port, CMD_PDC_READ_POWER_LEVEL);
return SMF_EVENT_HANDLED;
case SRC_ATTACHED_GET_VDO:
port->src_attached_local_state =
SRC_ATTACHED_GET_CABLE_PROPERTY;
queue_internal_cmd(port, CMD_PDC_GET_VDO);
return SMF_EVENT_HANDLED;
case SRC_ATTACHED_GET_CABLE_PROPERTY:
port->src_attached_local_state = SRC_ATTACHED_RUN;
queue_internal_cmd(port, CMD_PDC_GET_CABLE_PROPERTY);
return SMF_EVENT_HANDLED;
case SRC_ATTACHED_RUN:
run_src_policies(port);
break;
case SRC_ATTACHED_INVALID:
__builtin_unreachable();
}
return SMF_EVENT_HANDLED;
}
/**
* @brief Entering sink attached state
*/
static void pdc_snk_attached_entry(void *obj)
{
struct pdc_port_t *port = (struct pdc_port_t *)obj;
const struct pdc_config_t *config = port->dev->config;
int port_number = config->connector_num;
PRINT_STATE_WITH_SUBSTATE(port_number, get_pdc_state(port),
port->snk_attached_local_state,
snk_attached_local_state_names);
set_attached_pdc_state(port, SNK_ATTACHED_STATE);
port->send_cmd.intern.pending = false;
if (get_pdc_state(port) != port->send_cmd_return_state) {
/* Reset local state */
port->snk_attached_local_state =
SNK_ATTACHED_GET_CONNECTOR_CAPABILITY;
port->get_pdo = (struct get_pdo_t){ 0 };
/* If we were just a SRC, tell the DPM that the
* attached sink has been disconnected.
*/
pdc_dpm_remove_sink(port_number);
if (IS_ENABLED(CONFIG_PDC_POWER_MGMT_USB_MUX)) {
usb_mux_set(
port_number, USB_PD_MUX_USB_ENABLED,
USB_SWITCH_CONNECT,
pdc_power_mgmt_pd_get_polarity(port_number));
}
/* Update the PDC with the correct battery status. */
pdc_update_battery_capability(port);
pdc_update_battery_status(port, true);
}
}
static void pdc_print_pdo_info(int port, struct pdc_pdos_t *pdo)
{
uint32_t max_ma, max_mv, max_mw, min_mv;
const char *type_str = NULL;
if (pdo->pdo_count == 0 || pdo->pdo_count > PDO_MAX_OBJECTS) {
LOG_ERR("C%d: invalid pdo count detected %d", port,
pdo->pdo_count);
return;
}
/* Prints a table of PDOs with key fields extracted
*
* C0: Raw Type mV mA mW DRP UP USB DRD FRS
* C0: PDO1: 20019000, FIX 5000 0 0 Y - - - -
* C0: PDO2: 00000000, FIX 0 0 0 - - - - -
* ...
*/
LOG_INF("C%d: Raw Type mV mA mW "
"DRP UP USB DRD FRS",
port);
for (int i = 0; i < pdo->pdo_count; i++) {
uint32_t p = pdo->pdos[i];
if (p == 0) {
/* Empty PDO */
LOG_INF("C%d: PDO%d: %08x", port, i + 1, p);
continue;
}
pd_extract_pdo_power_unclamped(p, &max_ma, &max_mv, &min_mv);
max_mw = max_ma * max_mv / 1000;
switch (p & PDO_TYPE_MASK) {
case PDO_TYPE_FIXED:
type_str = "FIX";
LOG_INF("C%d: PDO%d: %08x, %s %-5u %-4u %-6u "
"%c %c %c %c %c",
port, i + 1, p, type_str, max_mv, max_ma,
max_mw, p & PDO_FIXED_DUAL_ROLE ? 'Y' : '-',
p & PDO_FIXED_UNCONSTRAINED ? 'Y' : '-',
p & PDO_FIXED_COMM_CAP ? 'Y' : '-',
p & PDO_FIXED_DATA_SWAP ? 'Y' : '-',
p & PDO_FIXED_FRS_CURR_MASK ? 'Y' : '-');
continue;
case PDO_TYPE_BATTERY:
type_str = "BAT";
break;
case PDO_TYPE_VARIABLE:
type_str = "VAR";
break;
/* LCOV_EXCL_START - unreachable as we filter augmented PDOs */
case PDO_TYPE_AUGMENTED:
type_str = "AUG";
break;
}
/* LCOV_EXCL_STOP */
/* Battery, variable, and augmented PDOs have voltage
* ranges but no flags.
*/
LOG_INF("C%d: PDO%d: %08x, %s %-5umV-%-5umV, %-5umA", port,
i + 1, p, type_str, min_mv, max_mv, max_ma);
}
}
static bool pdc_snk_policy_is_pdo_same(struct pdc_port_t *port,
uint32_t selected_port,
uint32_t selected_pdo, int pdo_index)
{
const struct pdc_config_t *const config = port->dev->config;
return (config->connector_num == selected_port &&
port->snk_policy.pdo == selected_pdo &&
port->snk_policy.pdo_index == (pdo_index + 1));
}
static void
pdc_snk_attached_send_set_rdo(struct pdc_port_t *port,
struct pdc_snk_attached_policy_t *snk_policy)
{
const struct pdc_config_t *const config = port->dev->config;
uint32_t max_ma, max_mv, max_mw, max_mw_pdo, unused;
uint32_t flags = RDO_COMM_CAP;
/* Get the unclamped PDO voltage and current to determine
* whether we need to set the capability mismatch bit if
* less power is offered than our operating requirement.
*/
pd_extract_pdo_power_unclamped(snk_policy->pdo, &max_ma, &max_mv,
&unused);
max_mw_pdo = max_ma * max_mv / 1000;
if (pdc_max_request_mv < CONFIG_PLATFORM_EC_USB_PD_MAX_VOLTAGE_MV) {
LOG_INF("C%d: max voltage (%d mV) limited by policy, don't set cap mismatch",
config->connector_num, pdc_max_request_mv);
} else if (max_mw_pdo < pdc_max_operating_power) {
flags |= RDO_CAP_MISMATCH;
}
/* Extract Current, Voltage, and calculate Power. Current is
* clamped to the board maximum here so that the RDO and charge
* manager are given the correct board operating current.
*/
pd_extract_pdo_power(snk_policy->pdo, &max_ma, &max_mv, &unused);
max_mw = max_ma * max_mv / 1000;
/* Set RDO to send */
if ((snk_policy->pdo & PDO_TYPE_MASK) == PDO_TYPE_BATTERY) {
snk_policy->rdo_to_send =
RDO_BATT(snk_policy->pdo_index, max_mw, max_mw, flags);
} else {
/* Fixed or variable RDO. */
snk_policy->rdo_to_send =
RDO_FIXED(snk_policy->pdo_index, max_ma, max_ma, flags);
}
LOG_INF("C%d: Send RDO: %d (%08x), battery_is_present=%d, mismatch=%d",
config->connector_num, RDO_POS(snk_policy->rdo_to_send),
snk_policy->rdo_to_send, battery_is_present(),
!!(flags & RDO_CAP_MISMATCH));
LOG_INF("C%d: Power request: %umA, %umV, %umW, "
"pdo_max=%umW, board_max=%umW",
config->connector_num, max_ma, max_mv, max_mw, max_mw_pdo,
pdc_max_operating_power);
queue_internal_cmd(port, CMD_PDC_SET_RDO);
}
static void pdc_snk_seed_charge_manager(struct pdc_port_t *port, uint32_t pdo)
{
const struct pdc_config_t *const config = port->dev->config;
uint32_t max_ma, max_mv, max_mw, unused;
pd_extract_pdo_power(pdo, &max_ma, &max_mv, &unused);
max_mw = max_ma * max_mv / 1000;
/* Only the fixed 5V PDO at index 0 has the UP and DRP bits set */
uint32_t vsafe_5v_pdo = port->snk_policy.partner_src_pdos.pdos[0];
LOG_INF("C%d: Available charging (%sconstrained)",
config->connector_num,
(vsafe_5v_pdo & PDO_FIXED_GET_UNCONSTRAINED_PWR) ? "un" : "");
LOG_INF(" PDO: %08x (RDO pos %d)", pdo,
RDO_POS(port->connector_status.rdo));
LOG_INF(" V: %d", max_mv);
LOG_INF(" I: %d", max_ma);
LOG_INF(" P: %d", max_mw);
if (port->sink_path_status) {
charge_manager_set_supplier(config->connector_num,
CHARGE_SUPPLIER_PD);
}
typec_set_input_current_limit(config->connector_num, 0, 0);
pd_set_input_current_limit(config->connector_num, max_ma, max_mv);
charge_manager_set_ceil(config->connector_num, CEIL_REQUESTOR_PD,
max_ma);
if (((PDO_GET_TYPE(pdo) == PDO_TYPE_FIXED) &&
(!(vsafe_5v_pdo & PDO_FIXED_GET_DRP) ||
(vsafe_5v_pdo & PDO_FIXED_GET_UNCONSTRAINED_PWR))) ||
(max_mw >= PD_DRP_CHARGE_POWER_MIN)) {
/* Port partner is a robust power source, meeting one or more of
* these conditions:
*
* 1) Able to provide PD_DRP_CHARGE_POWER_MIN watts or more,
* which defaults to 27W. This is likely a dock or large
* battery bank.
* 2) Partner advertised unconstrained power and is not DRP
* (this can only be determined from fixed PDOs). This is
* likely an AC power adapter.
*/
LOG_INF("C%d: Partner is a robust source (CAP_DEDICATED)",
config->connector_num);
charge_manager_update_dualrole(config->connector_num,
CAP_DEDICATED);
} else {
/* Port partner is a weak power source, such as a phone or small
* battery power bank. Charge manager may not initiate charging
* unless the user opts in to through the UI, or `chgoverride`
* is sent on the EC console.
*/
LOG_INF("C%d: Partner is a weak source (CAP_DUALROLE). Charging "
"may not start automatically!",
config->connector_num);
charge_manager_update_dualrole(config->connector_num,
CAP_DUALROLE);
}
}
static uint8_t pdc_get_snk_path_en_mask(void)
{
uint8_t snk_path_en_mask = 0;
for (int port = 0; port < CONFIG_USB_PD_PORT_MAX_COUNT; port++) {
WRITE_BIT(snk_path_en_mask, port,
pdc_data[port]->port.sink_path_status);
}
return snk_path_en_mask;
}
/** Return values for pdc_snk_attached_evaluate_pdos(), describing what
* action was taken by the function.
*/
enum eval_pdo_outcome {
/** Multiple sink paths are enabled. Hold in current state until only
* one sink path is active. */
EVAL_PDO_OUTCOME_SINK_PATH_STATE,
/** Keep the current RDO. A new RDO has NOT been sent, so proceed */
EVAL_PDO_OUTCOME_RETAIN_CURRENT_RDO,
/** A new RDO has been sent. Wait for the the PDC to re-negotiate before
* proceeding. */
EVAL_PDO_OUTCOME_SEND_NEW_RDO,
};
/**
* @brief Evaluate PDOs and send RDO when only one (or none) sink path is
* enabled. The sink path is disabled on non-preferred ports.
*
* @return enum eval_pdo_outcome
*/
static enum eval_pdo_outcome
pdc_snk_attached_evaluate_pdos(struct pdc_port_t *port)
{
const struct pdc_config_t *const config = port->dev->config;
int pdo_index = 0, selected_port;
uint32_t selected_pdo;
pdc_print_pdo_info(config->connector_num,
&port->snk_policy.partner_src_pdos);
pdo_index = pd_select_best_pdo(PDO_MAX_OBJECTS,
port->snk_policy.partner_src_pdos.pdos,
pdc_max_request_mv, &selected_pdo);
/* No valid PDOs found, move to next state */
if (pdo_index == -1) {
LOG_INF("C%d: No valid PDOs found. Keep current RDO.",
config->connector_num);
return EVAL_PDO_OUTCOME_RETAIN_CURRENT_RDO;
}
selected_port = charge_manager_get_active_charge_port();
if (pdc_snk_policy_is_pdo_same(port, selected_port, selected_pdo,
pdo_index)) {
LOG_INF("C%d: Retaining PDO[%d]=0x%08X", config->connector_num,
pdo_index, selected_pdo);
return EVAL_PDO_OUTCOME_RETAIN_CURRENT_RDO;
}
uint8_t sink_path_mask = pdc_get_snk_path_en_mask();
/* if sink path is enabled on more than 1 port */
if (sink_path_mask != 0 && !IS_POWER_OF_TWO(sink_path_mask)) {
LOG_INF("C%d: sink_path_mask=0x%X, selected_port=%d",
config->connector_num, sink_path_mask, selected_port);
/* Disable sink path if we're not the selected port. */
if (!IS_BIT_SET(sink_path_mask, selected_port)) {
port->sink_path_to_send = false;
queue_internal_cmd(port, CMD_PDC_SET_SINK_PATH);
}
/* Remain in current state until only one sink path is enabled*/
return EVAL_PDO_OUTCOME_SINK_PATH_STATE;
}
/* if sink path is enabled, battery is not present, and AP is ON,
* do not send RDO. Proceed to seed charge manager with current RDO.
*/
if (port->sink_path_status && battery_is_present() != BP_YES &&
!chipset_in_state(CHIPSET_STATE_HARD_OFF)) {
LOG_INF("C%d: Dead battery detected. Keep current RDO.",
config->connector_num);
return EVAL_PDO_OUTCOME_RETAIN_CURRENT_RDO;
}
/* Only one sink path is enabled, safe to update RDO */
port->snk_policy.pdo =
port->snk_policy.partner_src_pdos.pdos[pdo_index];
port->snk_policy.pdo_index = pdo_index + 1;
pdc_snk_attached_send_set_rdo(port, &port->snk_policy);
return EVAL_PDO_OUTCOME_SEND_NEW_RDO;
}
static bool pdc_is_rdo_valid(const union connector_status_t *cs)
{
LOG_INF("%s: status=%d, power_op_mode=%d, RDO_POS=%d", __func__,
cs->connect_status, cs->power_operation_mode, RDO_POS(cs->rdo));
return (cs->connect_status == 1 &&
cs->power_operation_mode == PD_OPERATION);
}
/**
* @brief Set the sink path handler for SNK_ATTACHED state
*
* Ensures the sink path is only enabled on one port at a time.
*
* @return true - Proceed to next state
* false - Remain in current state.
*/
static bool pdc_snk_attached_set_sink_path(struct pdc_port_t *port)
{
const struct pdc_config_t *const config = port->dev->config;
int selected_port = charge_manager_get_active_charge_port();
uint8_t sink_path_mask = pdc_get_snk_path_en_mask();
bool enable = selected_port == config->connector_num;
LOG_INF("C%d: sink_path_mask=0x%X, selected_port=%d, enable=%d",
config->connector_num, sink_path_mask, selected_port, enable);
if (enable) {
if (sink_path_mask == 0) {
/* No other ports have sink path enabled,
* proceed to enable */
int pdo_mv;
int pdo_ma;
int pdo_mw;
pd_extract_pdo_power_unclamped(port->snk_policy.pdo,
&pdo_ma, &pdo_mv,
&pdo_mw);
charge_manager_set_acokref(pdo_mv);
port->sink_path_to_send = true;
queue_internal_cmd(port, CMD_PDC_SET_SINK_PATH);
return true;
} else if (port->sink_path_status) {
/* Already enabled proceed to next state */
return true;
} else {
/* Sink path enabled on other ports, stay in current
* state and wait until they are disabled */
return false;
}
} else {
/* Disabling sink path, no need to wait */
port->sink_path_to_send = false;
queue_internal_cmd(port, CMD_PDC_SET_SINK_PATH);
return true;
}
}
/**
* @brief Run sink attached state.
*/
static enum smf_state_result pdc_snk_attached_run(void *obj)
{
struct pdc_port_t *port = (struct pdc_port_t *)obj;
const struct pdc_config_t *config = port->dev->config;
union conn_status_change_bits_t conn_status_change_bits;
/* The CCI_EVENT is set to re-query connector status, so check the
* connector status and take the appropriate action.
*/
if (atomic_test_and_clear_bit(port->cci_flags, CCI_EVENT)) {
queue_internal_cmd(port, CMD_PDC_GET_CONNECTOR_STATUS);
return SMF_EVENT_HANDLED;
}
if (atomic_test_and_clear_bit(port->cci_flags, CCI_VENDOR_EVENT)) {
queue_internal_cmd(port, CMD_PDC_GET_VENDOR_STATUS);
return SMF_EVENT_HANDLED;
}
if (atomic_test_and_clear_bit(port->cci_flags, CCI_ACK)) {
queue_internal_cmd(port, CMD_PDC_ACK_CC_CI);
return SMF_EVENT_HANDLED;
}
if (atomic_test_and_clear_bit(port->cci_flags, CCI_CAM_CHANGE)) {
queue_internal_cmd(port, CMD_PDC_GET_PD_VDO_DP_CFG_SELF);
return SMF_EVENT_HANDLED;
}
if (atomic_test_and_clear_bit(port->cci_flags, CCI_ATTENTION)) {
queue_internal_cmd(port, CMD_PDC_GET_ATTENTION_VDO);
return SMF_EVENT_HANDLED;
}
if (atomic_test_and_clear_bit(port->cci_flags, CCI_SET_SINK_PDOS)) {
LOG_INF("C%d: SNK attached send sink PDOs",
config->connector_num);
pdc_send_sink_pdos(port);
return SMF_EVENT_HANDLED;
}
/* If the attached charger sends new source caps, the PDC will
* turn off the sink path and we need to get the current
* source PDOs from the partner.
*/
if (port->snk_attached_local_state >= SNK_ATTACHED_GET_PDOS &&
atomic_test_and_clear_bit(port->snk_policy.flags,
SNK_POLICY_NEW_SRC_CAPS_AVAILABLE)) {
port->get_pdo = (struct get_pdo_t){ 0 };
invalidate_charger_settings(port, true);
/* Update the local state immediately without requiring
* a reschedule of the thread.
*/
port->snk_attached_local_state = SNK_ATTACHED_GET_PDOS;
}
switch (port->snk_attached_local_state) {
case SNK_ATTACHED_GET_CONNECTOR_CAPABILITY:
port->snk_attached_local_state =
SNK_ATTACHED_SET_DR_SWAP_POLICY;
queue_internal_cmd(port, CMD_PDC_GET_CONNECTOR_CAPABILITY);
return SMF_EVENT_HANDLED;
case SNK_ATTACHED_SET_DR_SWAP_POLICY:
port->snk_attached_local_state =
SNK_ATTACHED_SET_PR_SWAP_POLICY;
/* TODO read from DT */
port->uor.swap_to_dfp = 1;
port->uor.swap_to_ufp = 0;
port->uor.accept_dr_swap = 0;
queue_internal_cmd(port, CMD_PDC_SET_UOR);
return SMF_EVENT_HANDLED;
case SNK_ATTACHED_SET_PR_SWAP_POLICY:
port->snk_attached_local_state = SNK_ATTACHED_GET_VDO;
/* TODO: read from DT */
port->pdr_policy = PDC_POWER_POLICY_SINK(
port->snk_policy.accept_power_role_swap);
queue_internal_cmd(port, CMD_PDC_SET_PDR);
atomic_clear_bit(port->snk_policy.flags,
SNK_POLICY_UPDATE_ALLOW_PR_SWAP);
return SMF_EVENT_HANDLED;
case SNK_ATTACHED_GET_VDO:
port->snk_attached_local_state = SNK_ATTACHED_GET_PDOS;
queue_internal_cmd(port, CMD_PDC_GET_VDO);
return SMF_EVENT_HANDLED;
case SNK_ATTACHED_GET_PDOS:
/* Request up to 4 pdos to honor USCI 6.5.15 Get PDOs - Number
* of PDOs to return starting from the PDO Offset. The number of
* PDOs to return is the value in this field plus 1.
*/
atomic_clear_bit(port->snk_policy.flags,
SNK_POLICY_NEW_POWER_REQUEST);
atomic_clear_bit(port->snk_policy.flags,
SNK_POLICY_NEW_SRC_CAPS_AVAILABLE);
if (!port->get_pdo.updating) {
port->get_pdo.num_pdos = PDO_MAX_OBJECTS;
port->get_pdo.pdo_offset = PDO_OFFSET_0;
port->get_pdo.updating = true;
}
if (port->get_pdo.num_pdos > UCSI_GET_PDOS_MAX_NUM) {
port->snk_attached_local_state = SNK_ATTACHED_GET_PDOS;
} else {
port->snk_attached_local_state =
(port->sink_path_status ?
SNK_ATTACHED_SYNC_CHARGE_MGR :
SNK_ATTACHED_EVALUATE_PDOS);
port->get_pdo.updating = false;
}
port->get_pdo.pdo_type = SOURCE_PDO;
port->get_pdo.pdo_source = PARTNER_PDO;
queue_internal_cmd(port, CMD_PDC_GET_PDOS);
return SMF_EVENT_HANDLED;
case SNK_ATTACHED_EVALUATE_PDOS:
/* Remain in EVAL PDOs if more than one PDC has the sink path
* enabled. Once the charge manager is seeded, it will select
* the best port and disable the sink path on all but the best
* port.
*/
switch (pdc_snk_attached_evaluate_pdos(port)) {
case EVAL_PDO_OUTCOME_SINK_PATH_STATE:
/* Remain in this substate until only a single sink path
* is active. */
break;
case EVAL_PDO_OUTCOME_RETAIN_CURRENT_RDO:
/* No RDO changes. Proceed directly to seeding charge
* manager. */
port->snk_attached_local_state =
SNK_ATTACHED_SYNC_CHARGE_MGR;
break;
case EVAL_PDO_OUTCOME_SEND_NEW_RDO:
/* A new RDO was sent. Wait for the PD contract to be
* negotiated by the PDC. */
port->new_contract_timeout =
sys_timepoint_calc(NEW_CONTRACT_TIMEOUT);
port->snk_attached_local_state =
SNK_ATTACHED_WAIT_FOR_CONTRACT;
break;
}
return SMF_EVENT_HANDLED;
case SNK_ATTACHED_WAIT_FOR_CONTRACT:
/* Poll GET_CONNECTOR_STATUS until the `negotiated_power_level`
* bit is set. */
conn_status_change_bits.raw_value =
port->connector_status.raw_conn_status_change_bits;
LOG_INF("C%d: Wait for negotiated_power_level (%04x) "
"or correct RDO (req %d, curr %d)",
config->connector_num,
conn_status_change_bits.raw_value,
RDO_POS(port->snk_policy.rdo_to_send),
RDO_POS(port->connector_status.rdo));
if (conn_status_change_bits.negotiated_power_level ||
RDO_POS(port->snk_policy.rdo_to_send) ==
RDO_POS(port->connector_status.rdo)) {
/* New contract is in place. Seed charge_manager. */
port->snk_attached_local_state =
SNK_ATTACHED_SYNC_CHARGE_MGR;
} else if (sys_timepoint_expired(port->new_contract_timeout)) {
/* Timed out waiting for a contract. Proceed with the
* RDO chosen by the PDC, even if suboptimal. Charge
* manager will be seeded with the actual active RDO. */
LOG_ERR("C%d: New contract cannot be established. "
"Proceed with current contract.",
config->connector_num);
port->snk_attached_local_state =
SNK_ATTACHED_SYNC_CHARGE_MGR;
} else {
/* Poll GET_CONNECTOR_STATUS again */
queue_internal_cmd(port, CMD_PDC_GET_CONNECTOR_STATUS);
}
return SMF_EVENT_HANDLED;
case SNK_ATTACHED_SYNC_CHARGE_MGR:
/* Update Charge Manager with PDO/RDO the PDC has negotiated or
* that PDC Power Mgmt has evaluated. If Charge manager is not
* seeded, remain in this state. Charge manager will
* invoke set_active_charge_port after being seeded, moving
* PDC Power Mgmt onto SET_SINK_PATH state.
*/
if (pdc_is_rdo_valid(&port->connector_status)) {
uint32_t pdo_index =
RDO_POS(port->connector_status.rdo) - 1;
pdc_snk_seed_charge_manager(
port, port->snk_policy.partner_src_pdos
.pdos[pdo_index]);
}
port->snk_attached_local_state =
(charge_manager_is_seeded() ?
SNK_ATTACHED_SET_SINK_PATH :
SNK_ATTACHED_SYNC_CHARGE_MGR);
return SMF_EVENT_HANDLED;
case SNK_ATTACHED_SET_SINK_PATH:
if (pdc_snk_attached_set_sink_path(port)) {
if (pdc_power_mgmt_get_frs_hw_supported(
config->connector_num)) {
if (port->ccaps.op_mode_drp) {
port->snk_attached_local_state =
SNK_ATTACHED_GET_SINK_PDO;
} else {
pdc_dpm_remove_source(
config->connector_num);
port->snk_attached_local_state =
SNK_ATTACHED_GET_CABLE_PROPERTY;
}
} else {
port->snk_attached_local_state =
SNK_ATTACHED_GET_CABLE_PROPERTY;
}
}
return SMF_EVENT_HANDLED;
case SNK_ATTACHED_GET_SINK_PDO:
port->snk_attached_local_state =
SNK_ATTACHED_GET_CABLE_PROPERTY;
if (pdc_power_mgmt_get_frs_hw_supported(
config->connector_num)) {
/*
* We only care about the first fixed PDO for FRS, so
* only ask for the first sink PDO from the partner.
*/
port->get_pdo.num_pdos = 1;
port->get_pdo.pdo_offset = PDO_OFFSET_0;
port->get_pdo.pdo_type = SINK_PDO;
port->get_pdo.pdo_source = PARTNER_PDO;
port->get_pdo.updating = false;
/* Evaluate SNK CAP after it's been retrieved from the
* PDC */
atomic_set_bit(port->snk_policy.flags,
SNK_POLICY_EVAL_SNK_FIXED_PDO);
queue_internal_cmd(port, CMD_PDC_GET_PDOS);
return SMF_EVENT_HANDLED;
}
/* If
* !pdc_power_mgmt_get_frs_hw_supported(config->connector_num),
* fallthrough */
__fallthrough;
case SNK_ATTACHED_GET_CABLE_PROPERTY:
port->snk_attached_local_state = SNK_ATTACHED_READ_POWER_LEVEL;
queue_internal_cmd(port, CMD_PDC_GET_CABLE_PROPERTY);
return SMF_EVENT_HANDLED;
case SNK_ATTACHED_READ_POWER_LEVEL:
port->snk_attached_local_state = SNK_ATTACHED_RUN;
queue_internal_cmd(port, CMD_PDC_READ_POWER_LEVEL);
return SMF_EVENT_HANDLED;
case SNK_ATTACHED_RUN:
/* Hard Reset could disable Sink FET. Re-enable it */
if (atomic_get(&port->hard_reset_sent)) {
atomic_clear(&port->hard_reset_sent);
port->snk_attached_local_state =
SNK_ATTACHED_SET_SINK_PATH;
} else {
run_snk_policies(port);
}
break;
case SNK_ATTACHED_INVALID:
__builtin_unreachable();
}
return SMF_EVENT_HANDLED;
}
static void pdc_send_cmd_start_entry(void *obj)
{
struct pdc_port_t *port = (struct pdc_port_t *)obj;
port->send_cmd_return_state = port->last_state;
port->send_cmd.wait_counter = 0;
if (port->send_cmd.intern.pending) {
port->cmd = &port->send_cmd.intern;
} else {
port->cmd = &port->send_cmd.public;
}
}
/**
* @brief Return a string name for an `enum pdc_power_policy` value
*/
static const char *pdc_power_policy_to_string(enum pdc_power_policy policy)
{
switch (policy) {
case PDC_POWER_POLICY_SINK_ALLOW_SWAP:
return "SINK_ALLOW_SWAP";
case PDC_POWER_POLICY_SINK_DISALLOW_SWAP:
return "SINK_DISALLOW_SWAP";
case PDC_POWER_POLICY_SOURCE_ALLOW_SWAP:
return "SOURCE_ALLOW_SWAP";
case PDC_POWER_POLICY_SOURCE_DISALLOW_SWAP:
return "SOURCE_DISALLOW_SWAP";
}
return "Unknown";
}
static int send_pdc_cmd(struct pdc_port_t *port)
{
int rv;
const struct pdc_config_t *const config = port->dev->config;
uint32_t *rdo;
LOG_INF("C%d: Send %s (%d) %s", config->connector_num,
pdc_cmd_names[port->cmd->cmd], port->cmd->cmd,
(port->cmd == &port->send_cmd.intern) ? "internal" : "public");
/* Send PDC command via driver API */
switch (port->cmd->cmd) {
case CMD_PDC_RESET:
rv = pdc_reset(port->pdc);
break;
case CMD_PDC_GET_INFO:
rv = pdc_get_info(port->pdc, &port->info, true);
break;
case CMD_PDC_SET_POWER_LEVEL:
rv = pdc_set_power_level(port->pdc, port->una_policy.tcc);
break;
case CMD_PDC_SET_CCOM:
LOG_INF("C%d: SET_CCOM=%s", config->connector_num,
get_ccom_name(port->una_policy.cc_mode));
rv = pdc_set_ccom(port->pdc, port->una_policy.cc_mode);
break;
case CMD_PDC_SET_DRP:
LOG_INF("C%d: SET_DRP_MODE=%s", config->connector_num,
get_drp_mode_name(port->drp));
rv = pdc_set_drp_mode(port->pdc, port->drp);
break;
case CMD_PDC_GET_DRP:
rv = pdc_get_drp_mode(port->pdc, &port->drp_read);
break;
case CMD_PDC_GET_PDOS:
rv = pdc_get_pdos(port->pdc, port->get_pdo.pdo_type,
port->get_pdo.pdo_offset,
min(port->get_pdo.num_pdos,
UCSI_GET_PDOS_MAX_NUM),
port->get_pdo.pdo_source,
get_pdc_pdos_ptr(port, &port->get_pdo)->pdos +
port->get_pdo.pdo_offset);
if (!rv && port->get_pdo.num_pdos > UCSI_GET_PDOS_MAX_NUM) {
port->get_pdo.num_pdos -= UCSI_GET_PDOS_MAX_NUM;
port->get_pdo.pdo_offset = PDO_OFFSET_4;
}
break;
case CMD_PDC_GET_RDO:
/* RDO from LPM or port partner depending on power role */
if (port->attached_state == SRC_ATTACHED_STATE) {
rdo = &port->src_policy.rdo;
} else {
rdo = &port->snk_policy.rdo;
}
rv = pdc_get_rdo(port->pdc, rdo);
break;
case CMD_PDC_SET_RDO:
rv = pdc_set_rdo(port->pdc, port->snk_policy.rdo_to_send);
break;
case CMD_PDC_SET_SINK_PATH:
LOG_INF("C%d: sink_path_to_send=%d, chg_mgr_active_charge_port=%d",
config->connector_num, port->sink_path_to_send,
charge_manager_get_active_charge_port());
rv = pdc_set_sink_path(port->pdc, port->sink_path_to_send);
break;
case CMD_PDC_READ_POWER_LEVEL:
rv = pdc_read_power_level(port->pdc);
break;
case CMD_PDC_GET_CONNECTOR_CAPABILITY:
rv = pdc_get_connector_capability(port->pdc, &port->ccaps);
break;
case CMD_PDC_SET_UOR:
rv = pdc_set_uor(port->pdc, port->uor);
break;
case CMD_PDC_SET_PDR:
LOG_INF("C%d: Set PDR: %s", config->connector_num,
pdc_power_policy_to_string(port->pdr_policy));
rv = pdc_set_pdr(port->pdc, port->pdr_policy);
break;
case CMD_PDC_GET_CONNECTOR_STATUS:
rv = pdc_get_connector_status(port->pdc,
&port->connector_status);
break;
case CMD_PDC_GET_CABLE_PROPERTY:
rv = pdc_get_cable_property(port->pdc, &port->cable_prop);
break;
case CMD_PDC_GET_VDO:
rv = pdc_get_vdo(port->pdc, port->vdo_req, port->vdo_type,
port->vdo);
break;
case CMD_PDC_GET_PD_VDO_DP_CFG_SELF: {
union get_vdo_t vdo_req;
uint8_t vdo_type;
vdo_req.raw_value = 0;
vdo_req.num_vdos = 1;
vdo_req.vdo_origin = VDO_ORIGIN_PORT;
vdo_type = VDO_PD_DP_CFG;
rv = pdc_get_vdo(port->pdc, vdo_req, &vdo_type,
&port->vdo_dp_cfg);
break;
}
case CMD_PDC_GET_ATTENTION_VDO: {
rv = pdc_get_attention_vdo(port->pdc, &port->attention_vdo);
break;
}
case CMD_PDC_CONNECTOR_RESET:
rv = pdc_connector_reset(port->pdc, port->connector_reset);
break;
case CMD_PDC_GET_IDENTITY_DISCOVERY:
rv = pdc_get_identity_discovery(port->pdc,
&port->discovery_state);
break;
case CMD_PDC_IS_VCONN_SOURCING:
if (port->public_api_buff == NULL) {
return -EINVAL;
}
rv = pdc_is_vconn_sourcing(port->pdc,
(bool *)port->public_api_buff);
break;
case CMD_PDC_SET_PDOS:
rv = pdc_set_pdos(port->pdc, port->set_pdos.type,
port->set_pdos.pdos, port->set_pdos.count);
break;
case CMD_PDC_SET_FRS:
rv = pdc_set_frs(port->pdc, port->frs_enable);
break;
case CMD_PDC_GET_PCH_DATA_STATUS:
rv = pdc_get_pch_data_status(port->pdc, config->connector_num,
port->pch_data_status);
break;
case CMD_PDC_ACK_CC_CI:
rv = pdc_ack_cc_ci(port->pdc, port->ci, port->cc,
port->vendor_defined_ci);
break;
case CMD_PDC_GET_LPM_PPM_INFO:
rv = pdc_get_lpm_ppm_info(port->pdc, port->lpm_ppm_info);
break;
case CMD_PDC_GET_SBU_MUX_MODE:
rv = pdc_get_sbu_mux_mode(port->pdc, &port->sbu_mux_mode);
break;
case CMD_PDC_SET_SBU_MUX_MODE:
rv = pdc_set_sbu_mux_mode(port->pdc, port->sbu_mux_mode);
break;
case CMD_PDC_SET_AP_POWER_STATE:
rv = pdc_set_ap_power_state(port->pdc,
port->common_policy.ap_state);
break;
case CMD_PDC_SET_BBR_CTS:
rv = pdc_set_bbr_cts(port->pdc, port->bbr_cts_enable);
break;
case CMD_PDC_SET_BATTERY_STATUS:
rv = pdc_set_battery_status(port->pdc, &port->bstat);
break;
case CMD_PDC_SET_BATTERY_CAPABILITY:
rv = pdc_set_battery_capability(port->pdc, &port->bcap);
break;
case CMD_PDC_GET_VENDOR_STATUS:
rv = pdc_get_vendor_status(port->pdc,
&port->vendor_status_change);
break;
case CMD_PDC_GET_ALERT:
rv = pdc_get_alert(port->pdc, &port->ado);
break;
case CMD_PDC_SET_MAX_PDP:
if (pd_get_usb_pd_3a_ports() != 0)
rv = pdc_set_max_pdp(port->pdc, MAX_PDP_15W);
else
rv = pdc_set_max_pdp(port->pdc, MAX_PDP_7_5W);
break;
default:
LOG_ERR("C%d: Invalid command: %d", config->connector_num,
port->cmd->cmd);
return -EIO;
}
if (rv) {
LOG_DBG("C%d: Unable to send command: %s rv=%d",
config->connector_num, pdc_cmd_names[port->cmd->cmd],
rv);
}
return rv;
}
static enum smf_state_result pdc_send_cmd_start_run(void *obj)
{
struct pdc_port_t *port = (struct pdc_port_t *)obj;
const struct pdc_config_t *const config = port->dev->config;
int rv;
rv = send_pdc_cmd(port);
if (rv) {
LOG_DBG("C%d: Unable to send command: %s, rv=%d",
config->connector_num, pdc_cmd_names[port->cmd->cmd],
rv);
}
/*
* If the PDC is still processing a command (not in the IDLE state),
* then will remain in this state and CCI_CMD_COMPLETED can be set via
* the cci_event_cb function when the PDC driver finishes with the
* previous command, which previously didn't complete or fail within
* WAIT_MAX. This flag is only meaningful for the command that was just
* sent to the PDC.
*/
atomic_clear_bit(port->cci_flags, CCI_CMD_COMPLETED);
atomic_clear_bit(port->cci_flags, CCI_ERROR);
/* Command not implemented, no need to retry, return immediately */
if (rv == -ENOSYS) {
LOG_INF("C%d: Command (%s) not implemented",
config->connector_num, pdc_cmd_names[port->cmd->cmd]);
port->cmd->error = -ENOSYS;
port->cmd->pending = false;
set_pdc_state(port, port->send_cmd_return_state);
return SMF_EVENT_HANDLED;
}
/* Test if command was successful. If not, try again until max
* retries is reached */
else if (rv) {
port->send_cmd.wait_counter++;
if (port->send_cmd.wait_counter > WAIT_MAX) {
/* Could not send command: TODO handle error */
LOG_INF("C%d: Command (%s) retry timeout",
config->connector_num,
pdc_cmd_names[port->cmd->cmd]);
port->cmd->error = rv;
port->cmd->pending = false;
set_pdc_state(port, port->send_cmd_return_state);
}
return SMF_EVENT_HANDLED;
}
set_pdc_state(port, PDC_SEND_CMD_WAIT);
return SMF_EVENT_HANDLED;
}
static void pdc_send_cmd_wait_entry(void *obj)
{
struct pdc_port_t *port = (struct pdc_port_t *)obj;
port->send_cmd.wait_counter = 0;
port->send_cmd.resend_counter = 0;
}
static enum smf_state_result pdc_send_cmd_wait_run(void *obj)
{
struct pdc_port_t *port = (struct pdc_port_t *)obj;
const struct pdc_config_t *const config = port->dev->config;
/* Wait for command status notification from driver */
/*
* On a PDC_RESET, the PDC initiates an initializtion and the
* pdc_is_init_done() function is called to check if the initialization
* is complete
*/
if (port->cmd->cmd == CMD_PDC_RESET) {
if (pdc_is_init_done(port->pdc)) {
port->cmd->error = 0;
set_pdc_state(port, port->send_cmd_return_state);
return SMF_EVENT_HANDLED;
}
} else if (atomic_test_and_clear_bit(port->cci_flags, CCI_BUSY)) {
LOG_DBG("C%d: CCI_BUSY", config->connector_num);
} else if (atomic_test_and_clear_bit(port->cci_flags, CCI_ERROR)) {
LOG_DBG("C%d: CCI_ERROR", config->connector_num);
/* The PDC may set both error and complete bit */
atomic_clear_bit(port->cci_flags, CCI_CMD_COMPLETED);
/*
* TODO(b/325114016): Use ERROR_STATUS result to adjust the
* number of resend attempts. If the command being sent is
* either a SET_UOR or SET_PDR, then should have a lower (if
* any) number of resend attempts.
*/
if (port->send_cmd.resend_counter < CMD_RESEND_MAX) {
/* Try to resend command */
if (send_pdc_cmd(port) != 0) {
/*
* Set CCI_ERROR flag to trigger a resend of
* the pending command
*/
atomic_set_bit(port->cci_flags, CCI_ERROR);
} else {
/* PDC command resent, restart wait counter */
port->send_cmd.wait_counter = 0;
port->send_cmd.resend_counter++;
}
} else {
LOG_ERR("C%d: %s resend attempts exceeded!",
config->connector_num,
pdc_cmd_names[port->cmd->cmd]);
port->cmd->error = -EBUSY;
set_pdc_state(port, port->send_cmd_return_state);
return SMF_EVENT_HANDLED;
}
} else if (atomic_test_and_clear_bit(port->cci_flags,
CCI_CMD_COMPLETED)) {
LOG_DBG("C%d: CCI_CMD_COMPLETED", config->connector_num);
switch (port->cmd->cmd) {
case CMD_PDC_GET_CONNECTOR_STATUS:
handle_connector_status(port);
return SMF_EVENT_HANDLED;
case CMD_PDC_GET_ATTENTION_VDO:
handle_attention_vdo(port);
break;
case CMD_PDC_GET_VENDOR_STATUS:
handle_vendor_status(port);
break;
case CMD_PDC_GET_ALERT:
handle_alert(port, port->ado);
break;
default:
break;
}
set_pdc_state(port, port->send_cmd_return_state);
return SMF_EVENT_HANDLED;
/*
* Note: If the command was CONNECTOR_RESET, and the type of
* reset was a Hard Reset, then it would also make sense to
* notify the host of PD_STATUS_EVENT_HARD_RESET. However, this
* would be redundant with the notification that will be
* generated later, upon completion of GET_CONNECTOR_STATUS.
*/
} else {
/* No response: Wait until timeout. */
port->send_cmd.wait_counter++;
if (port->send_cmd.wait_counter > WAIT_MAX) {
port->cmd->error = -EBUSY;
if (port->cmd->cmd == CMD_PDC_GET_CONNECTOR_STATUS) {
/*
* Can't get connector status. Enter unattached
* state with error flag set, so it can reset
* the PDC.
*/
port->cmd->cmd = CMD_PDC_RESET;
set_pdc_state(port, PDC_UNATTACHED);
return SMF_EVENT_HANDLED;
} else {
set_pdc_state(port,
port->send_cmd_return_state);
return SMF_EVENT_HANDLED;
}
}
}
return SMF_EVENT_HANDLED;
}
static void pdc_send_cmd_wait_exit(void *obj)
{
struct pdc_port_t *port = (struct pdc_port_t *)obj;
struct pdc_pdos_t *pdc_pdos;
if (port->cmd == &port->send_cmd.public) {
k_event_post(&port->sm_event, PDC_PUBLIC_CMD_COMPLETE_EVENT);
}
/* Completed with error. Clear complete bit */
atomic_clear_bit(port->cci_flags, CCI_CMD_COMPLETED);
port->cmd->pending = false;
switch (port->cmd->cmd) {
case CMD_PDC_GET_PDOS:
/* Get pointer to struct for pdos array and count */
pdc_pdos = get_pdc_pdos_ptr(port, &port->get_pdo);
pdc_pdos->pdo_count = 0;
/* Filter out Augmented Power Data Objects (APDO). APDOs come
* after the regular PDOS, so it's safe to exclude them from the
* pdo_count. */
/* TODO This is temporary until APDOs can be handled */
for (int i = 0; i < PDO_MAX_OBJECTS; i++) {
if ((pdc_pdos->pdos[i] & PDO_TYPE_MASK) ==
PDO_TYPE_AUGMENTED) {
pdc_pdos->pdos[i] = 0;
} else {
pdc_pdos->pdo_count++;
}
}
break;
case CMD_PDC_SET_PDR:
case CMD_PDC_SET_UOR:
case CMD_PDC_SET_PDOS:
case CMD_PDC_SET_SINK_PATH:
case CMD_PDC_READ_POWER_LEVEL:
trigger_ppm_status_change(port);
break;
default:
break;
}
}
static void pdc_src_typec_only_entry(void *obj)
{
struct pdc_port_t *port = (struct pdc_port_t *)obj;
const struct pdc_config_t *config = port->dev->config;
int port_number = config->connector_num;
PRINT_STATE_WITH_SUBSTATE(port_number, get_pdc_state(port),
port->src_typec_attached_local_state,
src_typec_attached_local_state_names);
set_attached_pdc_state(port, SRC_ATTACHED_TYPEC_ONLY_STATE);
if (get_pdc_state(port) != port->send_cmd_return_state) {
invalidate_charger_settings(port, true);
port->src_typec_attached_local_state =
SRC_TYPEC_ATTACHED_SET_SINK_PATH_OFF;
/* Start one shot typec only timer. This timer is used to
* differentiate between a port partner that supports USB PD or
* is typec_only. Note that the timer is not explicitly
* stopped. Since there is no callback associated, letting it
* expire in the src.attached state will have no effect and the
* k_timer_start call always resets the timer status.
*/
k_timer_start(&port->typec_only_timer,
K_USEC(TYPEC_ONLY_SINK_DEBOUNCE_TIME_US),
K_NO_WAIT);
if (IS_ENABLED(CONFIG_PDC_POWER_MGMT_USB_MUX)) {
usb_mux_set(
port_number, USB_PD_MUX_USB_ENABLED,
USB_SWITCH_CONNECT,
pdc_power_mgmt_pd_get_polarity(port_number));
}
}
}
static enum smf_state_result pdc_src_typec_only_run(void *obj)
{
struct pdc_port_t *port = (struct pdc_port_t *)obj;
const struct pdc_config_t *config = port->dev->config;
int port_number = config->connector_num;
set_attached_pdc_state(port, SRC_ATTACHED_TYPEC_ONLY_STATE);
/* The CCI_EVENT is set on a connector disconnect, so check the
* connector status and take the appropriate action. */
if (atomic_test_and_clear_bit(port->cci_flags, CCI_EVENT)) {
queue_internal_cmd(port, CMD_PDC_GET_CONNECTOR_STATUS);
return SMF_EVENT_HANDLED;
}
if (atomic_test_and_clear_bit(port->cci_flags, CCI_ACK)) {
queue_internal_cmd(port, CMD_PDC_ACK_CC_CI);
return SMF_EVENT_HANDLED;
}
if (atomic_test_and_clear_bit(port->cci_flags, CCI_SET_SINK_PDOS)) {
LOG_INF("C%d: SRC Type-C send sink PDOs",
config->connector_num);
pdc_send_sink_pdos(port);
return SMF_EVENT_HANDLED;
}
switch (port->src_typec_attached_local_state) {
case SRC_TYPEC_ATTACHED_SET_SINK_PATH_OFF:
port->src_typec_attached_local_state =
SRC_TYPEC_ATTACHED_DEBOUNCE;
port->sink_path_to_send = false;
queue_internal_cmd(port, CMD_PDC_SET_SINK_PATH);
return SMF_EVENT_HANDLED;
case SRC_TYPEC_ATTACHED_DEBOUNCE:
if (k_timer_status_get(&port->typec_only_timer) > 0) {
port->src_typec_attached_local_state =
SRC_TYPEC_ATTACHED_ADD_SINK;
}
return SMF_EVENT_HANDLED;
case SRC_TYPEC_ATTACHED_ADD_SINK:
port->src_typec_attached_local_state =
SRC_TYPEC_READ_POWER_LEVEL;
/* Notify DPM that a type-c only port partner is attached */
pdc_dpm_add_non_pd_sink(port_number);
return SMF_EVENT_HANDLED;
case SRC_TYPEC_READ_POWER_LEVEL:
port->src_typec_attached_local_state = SRC_TYPEC_ATTACHED_RUN;
queue_internal_cmd(port, CMD_PDC_READ_POWER_LEVEL);
break;
case SRC_TYPEC_ATTACHED_RUN:
run_typec_src_policies(port);
break;
case SRC_TYPEC_ATTACHED_INVALID:
__builtin_unreachable();
}
return SMF_EVENT_HANDLED;
}
static void pdc_snk_typec_only_entry(void *obj)
{
struct pdc_port_t *port = (struct pdc_port_t *)obj;
const struct pdc_config_t *config = port->dev->config;
int port_number = config->connector_num;
port->send_cmd.intern.pending = false;
if (get_pdc_state(port) != port->send_cmd_return_state) {
port->snk_typec_attached_local_state =
SNK_TYPEC_ATTACHED_DEBOUNCE;
/* Start one shot typec only timer. This timer is used to
* differentiate between a port partner that supports USB PD or
* is typec_only. Note that the timer is not explicitly
* stopped. Since there is no callback associated, letting it
* expire in the snk.attached state will have no effect and the
* k_timer_start call always resets the timer status.
*/
k_timer_start(&port->typec_only_timer,
K_USEC(PD_T_SINK_WAIT_CAP), K_NO_WAIT);
if (IS_ENABLED(CONFIG_PDC_POWER_MGMT_USB_MUX)) {
usb_mux_set(
port_number, USB_PD_MUX_USB_ENABLED,
USB_SWITCH_CONNECT,
pdc_power_mgmt_pd_get_polarity(port_number));
}
}
PRINT_STATE_WITH_SUBSTATE(port_number, get_pdc_state(port),
port->snk_typec_attached_local_state,
snk_typec_attached_local_state_names);
set_attached_pdc_state(port, SNK_ATTACHED_TYPEC_ONLY_STATE);
}
static enum smf_state_result pdc_snk_typec_only_run(void *obj)
{
struct pdc_port_t *port = (struct pdc_port_t *)obj;
const struct pdc_config_t *const config = port->dev->config;
set_attached_pdc_state(port, SNK_ATTACHED_TYPEC_ONLY_STATE);
/* The CCI_EVENT is set to re-query connector status, so check the
* connector status and take the appropriate action.
*/
if (atomic_test_and_clear_bit(port->cci_flags, CCI_EVENT)) {
queue_internal_cmd(port, CMD_PDC_GET_CONNECTOR_STATUS);
return SMF_EVENT_HANDLED;
}
if (atomic_test_and_clear_bit(port->cci_flags, CCI_ACK)) {
queue_internal_cmd(port, CMD_PDC_ACK_CC_CI);
return SMF_EVENT_HANDLED;
}
if (atomic_test_and_clear_bit(port->cci_flags, CCI_SET_SINK_PDOS)) {
LOG_INF("C%d: SNK Type-C send sink PDOs",
config->connector_num);
pdc_send_sink_pdos(port);
return SMF_EVENT_HANDLED;
}
switch (port->snk_typec_attached_local_state) {
case SNK_TYPEC_ATTACHED_DEBOUNCE:
if (k_timer_status_get(&port->typec_only_timer) > 0) {
port->snk_typec_attached_local_state =
SNK_TYPEC_ATTACHED_SET_CHARGE_CURRENT;
}
return SMF_EVENT_HANDLED;
case SNK_TYPEC_ATTACHED_SET_CHARGE_CURRENT:
port->snk_typec_attached_local_state =
SNK_TYPEC_READ_POWER_LEVEL;
/* Once we're updating the charger with the new current limit,
* it's safe to clear the policy bit. If the PDC reports
* a new change to power operation mode, this but will be
* set again.
*/
atomic_clear_bit(port->snk_policy.flags,
SNK_POLICY_UPDATE_TYPEC_CURRENT);
if (port->sink_path_status) {
charge_manager_set_supplier(config->connector_num,
CHARGE_SUPPLIER_TYPEC);
}
pd_set_input_current_limit(config->connector_num, 0, 0);
typec_set_input_current_limit(config->connector_num,
port->typec_current_ma, 5000);
charge_manager_update_dualrole(config->connector_num,
CAP_DEDICATED);
break;
case SNK_TYPEC_READ_POWER_LEVEL:
port->snk_typec_attached_local_state = SNK_TYPEC_ATTACHED_RUN;
queue_internal_cmd(port, CMD_PDC_READ_POWER_LEVEL);
break;
case SNK_TYPEC_ATTACHED_RUN:
run_typec_snk_policies(port);
break;
case SNK_TYPEC_ATTACHED_INVALID:
__builtin_unreachable();
}
return SMF_EVENT_HANDLED;
}
static void pdc_init_entry(void *obj)
{
struct pdc_port_t *port = (struct pdc_port_t *)obj;
const struct pdc_config_t *config = port->dev->config;
int port_number = config->connector_num;
PRINT_STATE_WITH_SUBSTATE(port_number, get_pdc_state(port),
port->init_local_state,
init_local_state_names);
if (get_pdc_state(port) != port->send_cmd_return_state) {
/* True if entering the init state for the first time, as
* opposed to returning from a completed command called by one
* of the local states (sub-steps)
*/
/* Initialize Send Command data */
send_cmd_init(port);
/* Set up GET_VDO command data */
discovery_info_init(port);
if (IS_ENABLED(CONFIG_PDC_POWER_MGMT_USB_MUX)) {
usb_mux_init(port_number);
}
port->init_local_state = INIT_WAIT_FOR_READY;
port->public_api_buff = NULL;
}
}
static void pdc_notify_ap_power_state(int port, enum power_state state)
{
pdc_data[port]->port.common_policy.ap_state = state;
atomic_set_bit(pdc_data[port]->port.common_policy.flags,
COMMON_POLICY_SET_POWER_STATE);
LOG_INF("C%d: Setting AP power state to %s", port,
state == POWER_S0 ? "S0" : "S5");
}
/**
* @brief Chipset Resume (S3->S0) Policy 1: Set a flag to perform a one-time
* test if we should swap to a source role. (applicable only if we are
* currently a sink)
*/
static void enforce_pd_chipset_resume_policy_1(int port)
{
LOG_DBG("C%d: Chipset Resume Policy 1", port);
/* If we're in a sink role, run a check to determine if we'd prefer a
* source role.
*/
atomic_set_bit(pdc_data[port]->port.snk_policy.flags,
SNK_POLICY_EVAL_SWAP_TO_SRC);
}
/*
* PD policy handlers
*
* These functions are triggered by AP power state changes via hooks and also
* through the PDC power management state machine's init state in cases when a
* late system jump happened.
*
* These functions should set flags to trigger actions from within the state
* machine, rather than performing operations directly.
*/
/**
* @brief Chipset Resume (S3->S0) Policy 2:
* a) Set DRP role based on platform
*/
static void enforce_pd_chipset_resume_policy_2(int port)
{
LOG_DBG("C%d: Chipset Resume Policy 2", port);
pdc_power_mgmt_set_dual_role(port, pd_get_drp_state_in_s0());
}
/**
* @brief Chipset Suspend (S0->S3) Policy 1:
* a) DRP TOGGLE OFF
*/
static void enforce_pd_chipset_suspend_policy_1(int port)
{
LOG_DBG("C%d: Chipset Suspend Policy 1", port);
atomic_clear_bit(pdc_data[port]->port.snk_policy.flags,
SNK_POLICY_EVAL_SWAP_TO_SRC);
pdc_power_mgmt_set_dual_role(port, PD_DRP_TOGGLE_OFF);
}
/**
* Chipset Shutdown (S3->S5) Policy 1:
* a) DRP Force SINK
*/
static void enforce_pd_chipset_shutdown_policy_1(int port)
{
LOG_DBG("C%d: Chipset Shutdown Policy 1", port);
pdc_power_mgmt_set_dual_role(port, PD_DRP_FORCE_SINK);
/* Notify PDC that AP is shutting down to disable retimer */
pdc_notify_ap_power_state(port, POWER_S5);
}
static void set_hpd_wake_watch(int port);
static void clear_hpd_wake_watch(int port);
/**
* @brief Apply correct policy based on system power state
*
* This is normally triggered by hooks on AP power state changes
* (HOOK_CHIPSET_RESUME, etc) elsewhere in this file. The hooks enforce
* hysteresis on the power state to avoid rapid policy flapping.
*
* In the case of a late sysjump, this function is also called during
* init to force the correct policy, since the normal start-up power
* state transition hooks will not be occur.
*
* Note: this should run once, and not per-port.
*/
static void pdc_apply_power_state_policy(struct k_work *work)
{
uint8_t port_count = pdc_power_mgmt_get_usb_pd_port_count();
if (chipset_in_state(CHIPSET_STATE_ON)) {
LOG_INF("PD: AP is ON: apply 'resume' policy");
for (int i = 0; i < port_count; i++) {
/* Notify PDC that the AP is in S0 to enable retimers */
pdc_notify_ap_power_state(i, POWER_S0);
/*
* Setting the dual role state clears the policy flag
* SNK_POLICY_SWAP_TO_SRC which may get set in
* enforce_pd_chipset_resume_policy_1() so this policy
* function needs to be called after resume_policy_2()
* which sets DRP mode on.
*/
enforce_pd_chipset_resume_policy_2(i);
enforce_pd_chipset_resume_policy_1(i);
clear_hpd_wake_watch(i);
}
} else if (chipset_in_state(CHIPSET_STATE_ANY_SUSPEND)) {
LOG_INF("PD: AP is SUSPENDED: apply 'suspend' policy");
for (int i = 0; i < port_count; i++) {
enforce_pd_chipset_suspend_policy_1(i);
set_hpd_wake_watch(i);
}
} else if (chipset_in_state(CHIPSET_STATE_ANY_OFF)) {
LOG_INF("PD: AP is OFF: apply 'shutdown' policy");
for (int i = 0; i < port_count; i++) {
enforce_pd_chipset_shutdown_policy_1(i);
}
}
}
static K_WORK_DELAYABLE_DEFINE(pdc_apply_power_state_policy_work,
pdc_apply_power_state_policy);
/**
* @brief Returns true if all PDC port drivers have finished initializing
*
* @return bool True if all ports are ready, false if still pending.
*/
static bool pdc_all_ports_ready(void)
{
int num_ports = pdc_power_mgmt_get_usb_pd_port_count();
for (uint8_t i = 0; i < num_ports; i++) {
if (!pdc_is_init_done(pdc_data[i]->port.pdc)) {
return false;
}
}
return true;
}
static enum smf_state_result pdc_init_run(void *obj)
{
struct pdc_port_t *port = (struct pdc_port_t *)obj;
const struct pdc_config_t *const config = port->dev->config;
switch (port->init_local_state) {
case INIT_WAIT_FOR_READY:
if (!pdc_is_init_done(port->pdc)) {
/* Still waiting on driver to be ready */
return SMF_EVENT_HANDLED;
}
LOG_INF("C%d: PDC Subsystem Started", config->connector_num);
/* Apply policy in case of a late sysjump since we won't receive
* the usual hook calls upon AP power state changes. Only called
* once, after all port drivers are ready.
*/
if (system_jumped_late() && pdc_all_ports_ready()) {
LOG_INF("PD: Handling late sysjump");
pdc_apply_power_state_policy(
&pdc_apply_power_state_policy_work.work);
}
port->init_local_state = INIT_SET_SBU_MUX_FORCED_DEBUG;
/* Proceed directly to next sub-state */
__fallthrough;
case INIT_SET_SBU_MUX_FORCED_DEBUG:
port->init_local_state = INIT_SET_SINK_PDOS;
if (atomic_test_and_clear_bit(
port->common_policy.flags,
COMMON_POLICY_SET_SBU_MUX_TO_FORCED_DEBUG)) {
/* Set the SBU mux operating mode to forced-debug */
port->sbu_mux_mode = PDC_SBU_MUX_MODE_FORCE_DBG;
queue_internal_cmd(port, CMD_PDC_SET_SBU_MUX_MODE);
break;
}
/* If flag is unset, proceed directly to next sub-state */
__fallthrough;
case INIT_SET_SINK_PDOS:
port->init_local_state = INIT_SET_SRC_PDOS;
pdc_send_sink_pdos(port);
queue_internal_cmd(port, CMD_PDC_SET_PDOS);
break;
case INIT_SET_SRC_PDOS:
port->init_local_state = INIT_SET_FRS;
port->attached_state = INIT_STATE;
pdc_power_mgmt_set_current_limit(
config->connector_num,
pdc_power_mgmt_get_default_current_limit(
config->connector_num));
port->set_pdos = (struct set_pdos_t){
.count = 1,
.type = SOURCE_PDO,
.pdos = { port->src_policy.lpm_src_pdo },
};
queue_internal_cmd(port, CMD_PDC_SET_PDOS);
break;
case INIT_SET_FRS:
port->init_local_state = INIT_SET_MAX_PDP;
if (pdc_power_mgmt_get_frs_hw_supported(
config->connector_num)) {
port->frs_enable = true;
} else {
port->frs_enable = false;
}
queue_internal_cmd(port, CMD_PDC_SET_FRS);
break;
case INIT_SET_MAX_PDP:
port->init_local_state = INIT_GET_CONNECTOR_STATUS;
queue_internal_cmd(port, CMD_PDC_SET_MAX_PDP);
break;
case INIT_GET_CONNECTOR_STATUS:
/* Send the connector status command to determine which state to
* enter
*/
queue_internal_cmd(port, CMD_PDC_GET_CONNECTOR_STATUS);
break;
case INIT_INVALID:
__builtin_unreachable();
}
return SMF_EVENT_HANDLED;
}
static void pdc_suspended_entry(void *obj)
{
struct pdc_port_t *port = (struct pdc_port_t *)obj;
const struct pdc_config_t *const config = port->dev->config;
PRINT_STATE(config->connector_num, get_pdc_state(port));
}
static enum smf_state_result pdc_suspended_run(void *obj)
{
struct pdc_port_t *port = (struct pdc_port_t *)obj;
if (atomic_get(&port->suspend)) {
/* Still suspended. Do nothing. */
return SMF_EVENT_HANDLED;
}
/* No longer suspended. Do a full reset. */
init_port_variables(port, true);
set_pdc_state(port, PDC_INIT);
return SMF_EVENT_HANDLED;
}
/**
* @brief Populate state table
*/
static const struct smf_state pdc_states[] = {
/* Normal States */
[PDC_INIT] = SMF_CREATE_STATE(pdc_init_entry, pdc_init_run, NULL, NULL,
NULL),
[PDC_UNATTACHED] = SMF_CREATE_STATE(
pdc_unattached_entry, pdc_unattached_run, NULL, NULL, NULL),
[PDC_SNK_ATTACHED] = SMF_CREATE_STATE(
pdc_snk_attached_entry, pdc_snk_attached_run, NULL, NULL, NULL),
[PDC_SRC_ATTACHED] = SMF_CREATE_STATE(
pdc_src_attached_entry, pdc_src_attached_run, NULL, NULL, NULL),
[PDC_SEND_CMD_START] = SMF_CREATE_STATE(pdc_send_cmd_start_entry,
pdc_send_cmd_start_run, NULL,
NULL, NULL),
[PDC_SEND_CMD_WAIT] =
SMF_CREATE_STATE(pdc_send_cmd_wait_entry, pdc_send_cmd_wait_run,
pdc_send_cmd_wait_exit, NULL, NULL),
[PDC_SRC_TYPEC_ONLY] = SMF_CREATE_STATE(pdc_src_typec_only_entry,
pdc_src_typec_only_run, NULL,
NULL, NULL),
[PDC_SNK_TYPEC_ONLY] = SMF_CREATE_STATE(pdc_snk_typec_only_entry,
pdc_snk_typec_only_run, NULL,
NULL, NULL),
[PDC_SUSPENDED] = SMF_CREATE_STATE(pdc_suspended_entry,
pdc_suspended_run, NULL, NULL, NULL),
[PDC_DISABLED] = SMF_CREATE_STATE(NULL, NULL, NULL, NULL, NULL),
};
/**
* Do not use for internal checks since port counts are not known until all
* pdc_power_mgmt devices initialize.
*/
bool pdc_power_mgmt_is_pdc_port_valid(int port)
{
return (port >= 0) && (port < pdc_power_mgmt_get_usb_pd_port_count()) &&
(get_pdc_state(&pdc_data[port]->port) != PDC_DISABLED);
}
/**
* @brief CCI event handler call back
*/
static void pdc_cc_handler_cb(const struct device *dev,
const struct pdc_callback *callback,
union cci_event_t cci_event)
{
struct pdc_port_t *port =
CONTAINER_OF(callback, struct pdc_port_t, cc_cb);
bool post_event = false;
/* Handle busy event from driver */
if (cci_event.busy) {
atomic_set_bit(port->cci_flags, CCI_BUSY);
post_event = true;
}
/* Handle error event from driver */
if (cci_event.error) {
atomic_set_bit(port->cci_flags, CCI_ERROR);
post_event = true;
}
/* Handle command completed event from driver */
if (cci_event.command_completed) {
atomic_set_bit(port->cci_flags, CCI_CMD_COMPLETED);
post_event = true;
}
if (post_event)
k_event_post(&port->sm_event, PDC_SM_EVENT);
}
static void pdc_ci_handler_cb(const struct device *dev,
const struct pdc_callback *callback,
union cci_event_t cci_event)
{
struct pdc_port_t *port =
CONTAINER_OF(callback, struct pdc_port_t, ci_cb);
const struct pdc_config_t *const config = port->dev->config;
bool post_event = false;
/* Handle generic vendor defined event from driver */
if (cci_event.vendor_defined_indicator) {
atomic_set_bit(port->cci_flags, CCI_EVENT);
atomic_set_bit(port->cci_flags, CCI_VENDOR_EVENT);
post_event = true;
}
if (cci_event.connector_change == config->connector_num + 1) {
/* Clear the status ready event before setting the PPM event
* bit. This will correctly order calls to grab the cached
* connector status in
* |pdc_power_mgmt_get_connector_status_for_ppm|.
*/
k_event_clear(&port->settle_event,
PDC_PPM_CONNECTOR_STATUS_READY);
atomic_set_bit(port->cci_flags, CCI_PPM_EVENT);
post_event = true;
}
if (post_event)
k_event_post(&port->sm_event, PDC_SM_EVENT);
}
static void init_port_variables(struct pdc_port_t *port,
bool reset_charge_manager)
{
/* This optionally seeds the Charge Manager */
invalidate_charger_settings(port, reset_charge_manager);
/* Init port variables */
atomic_clear(port->pdc_cmd_flags);
atomic_clear(port->cci_flags);
port->port_event = ATOMIC_INIT(0);
port->get_pdo.updating = false;
port->last_state = PDC_INIT;
port->next_state = PDC_INIT;
port->send_cmd_return_state = PDC_INVALID;
discovery_info_init(port);
}
/**
* @brief Initialize the PDC Subsystem
*/
static int pdc_subsys_init(const struct device *dev)
{
struct pdc_data_t *data = dev->data;
struct pdc_port_t *port = &data->port;
const struct pdc_config_t *const config = dev->config;
int rv;
const struct device *pdc =
pdc_power_mgmt_get_port_pdc_driver(config->connector_num);
#ifdef CONFIG_PDC_RUNTIME_PORT_CONFIG
/* Runtime-defined PDC configuration. Need to initialize this PDC
* driver here manually. */
uint8_t active_port_count = pdc_power_mgmt_get_usb_pd_port_count();
if (config->connector_num >= active_port_count || pdc == NULL) {
LOG_ERR("C%d: Port disabled per board configuration "
"(total active ports: %u)",
config->connector_num, active_port_count);
goto disable_port;
}
/* Try initializing this driver */
rv = device_init(pdc);
if (rv) {
LOG_ERR("C%d: Cannot initialize PDC: %d (PDC %p %s). "
"Disabling this port",
config->connector_num, rv, pdc,
pdc->name ? pdc->name : "<no name>");
goto disable_port;
}
#else /* !defined(CONFIG_PDC_RUNTIME_PORT_CONFIG) */
/* Static PDC configuration. Make sure the assigned PDC is ready. */
if (!device_is_ready(pdc)) {
LOG_ERR_DEVICE_NOT_READY(pdc);
goto disable_port;
}
#endif /* !defined(CONFIG_PDC_RUNTIME_PORT_CONFIG) */
port->pdc = pdc;
init_port_variables(port, false);
port->drp = port->una_policy.drp_mode;
/* Set cc call back */
port->cc_cb.handler = pdc_cc_handler_cb;
pdc_set_cc_callback(port->pdc, &port->cc_cb);
/* Set ci call back */
port->ci_cb.handler = pdc_ci_handler_cb;
rv = pdc_add_ci_callback(port->pdc, &port->ci_cb);
if (rv)
LOG_ERR("C%d: Failed to add CI callback (%d)",
config->connector_num, rv);
/* Initialize state machine run event */
k_event_init(&port->sm_event);
k_event_init(&port->settle_event);
/* Initialize command mutex */
k_mutex_init(&port->mtx);
smf_set_initial(&port->ctx, &pdc_states[PDC_INIT]);
/* Initialize typec only timer */
k_timer_init(&port->typec_only_timer, NULL, NULL);
/* Initialize platform policy */
enforce_pd_chipset_shutdown_policy_1(config->connector_num);
/* Create the thread for this port */
config->create_thread(dev);
return 0;
disable_port:
/* Prevent sending public API commands. Note: we never create a
* driver thread in this code path, so nothing will happen for
* this port. */
smf_set_initial(&port->ctx, &pdc_states[PDC_DISABLED]);
return -ENODEV;
}
/**
* @brief Start the PDC subsystem and the PDC driver threads
*/
void pdc_subsys_start(void)
{
for (int port = 0; port < ARRAY_SIZE(pdc_data); port++) {
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
/* Skip over inactive ports */
continue;
}
/* Start the PDC driver threads */
if (device_is_ready(pdc_data[port]->port.pdc)) {
pdc_start_thread(pdc_data[port]->port.pdc);
}
/* Start the PDC power management threads */
if (device_is_ready(pdc_data[port]->port.dev)) {
k_thread_start(pdc_data[port]->thread);
}
}
}
/**
* @brief Returns true if command can be executed without a port partner
* connection
*/
static bool is_connectionless_cmd(enum pdc_cmd_t pdc_cmd)
{
switch (pdc_cmd) {
case CMD_PDC_RESET:
__fallthrough;
case CMD_PDC_SET_POWER_LEVEL:
__fallthrough;
case CMD_PDC_GET_INFO:
__fallthrough;
case CMD_PDC_GET_PCH_DATA_STATUS:
__fallthrough;
case CMD_PDC_SET_DRP:
__fallthrough;
case CMD_PDC_GET_DRP:
__fallthrough;
case CMD_PDC_GET_LPM_PPM_INFO:
__fallthrough;
case CMD_PDC_GET_SBU_MUX_MODE:
__fallthrough;
case CMD_PDC_SET_BBR_CTS:
__fallthrough;
case CMD_PDC_SET_SBU_MUX_MODE:
__fallthrough;
case CMD_PDC_SET_FRS:
return true;
default:
return false;
}
}
/**
* @brief Called from a public API function to block until the command completes
* or time outs
*/
static int public_api_block(int port, enum pdc_cmd_t pdc_cmd)
{
int ret;
struct cmd_t *public_cmd;
k_timepoint_t cmd_timepoint;
ret = queue_public_cmd(&pdc_data[port]->port, pdc_cmd);
if (ret) {
LOG_ERR("C%d: Could not queue %s: %d", port,
pdc_cmd_names[pdc_cmd], ret);
return ret;
}
public_cmd = &pdc_data[port]->port.send_cmd.public;
/* TODO: Investigate using a semaphore here instead of while loop */
/* Block calling thread until command is processed, errors or timeout
* occurs. */
cmd_timepoint = sys_timepoint_calc(K_MSEC(PDC_CMD_TIMEOUT_MS));
while (public_cmd->pending && !public_cmd->error) {
/* block until command completes or max block count is reached
*/
/* Wait for timeout or event */
ret = k_event_wait(&pdc_data[port]->port.sm_event,
PDC_PUBLIC_CMD_COMPLETE_EVENT, false,
K_MSEC(PUBLIC_CMD_DELAY_MS));
if (ret != 0) {
k_event_clear(&pdc_data[port]->port.sm_event,
PDC_PUBLIC_CMD_COMPLETE_EVENT);
}
if (sys_timepoint_expired(cmd_timepoint)) {
/* something went wrong */
LOG_ERR("C%d: Public API blocking timeout: %s", port,
pdc_cmd_names[public_cmd->cmd]);
public_cmd->pending = false;
return -EBUSY;
}
/* Check for commands that don't require a connection */
if (is_connectionless_cmd(public_cmd->cmd)) {
continue;
}
/* The system is blocking on a command that requires a
* connection, so return if disconnected */
if (!pdc_power_mgmt_is_connected(port)) {
LOG_ERR("C%d: Command %s requires connection", port,
pdc_cmd_names[public_cmd->cmd]);
return -EIO;
}
}
if (public_cmd->error) {
LOG_ERR("C%d: Public API command %s not sent, err=%d", port,
pdc_cmd_names[public_cmd->cmd], public_cmd->error);
return public_cmd->error;
}
return 0;
}
/**
* PDC Power Management Public API
*/
static bool pdc_power_mgmt_is_sink_connected(int port)
{
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return false;
}
return pdc_data[port]->port.attached_state == SNK_ATTACHED_STATE;
}
static bool pdc_power_mgmt_is_source_connected(int port)
{
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return false;
}
return pdc_data[port]->port.attached_state == SRC_ATTACHED_STATE;
}
test_mockable bool pdc_power_mgmt_is_connected(int port)
{
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return false;
}
return pdc_data[port]->port.attached_state != UNATTACHED_STATE;
}
#ifndef CONFIG_PDC_RUNTIME_PORT_CONFIG
/*
* When static port config is used (normal), each named-usbc-port must have
* only one PDC phandle entry, and it must not be marked zephyr,deferred-init.
*/
#define CHECK_NAMED_USBC_PORT(inst) \
BUILD_ASSERT(1 == DT_INST_PROP_LEN(inst, pdc)); \
BUILD_ASSERT(0 == DT_PROP(DT_INST_PROP_BY_IDX(inst, pdc, 0), \
zephyr_deferred_init));
#ifndef CONFIG_ZTEST
/* Only enforce this on real builds so tests can try some failure scenarios */
DT_INST_FOREACH_STATUS_OKAY(CHECK_NAMED_USBC_PORT)
#endif /* !defined(CONFIG_ZTEST) */
/* Find the associated PDC driver using static devicetree information */
const struct device *pdc_power_mgmt_get_port_pdc_driver(int port)
{
if (port < 0 || port >= CONFIG_USB_PD_PORT_MAX_COUNT) {
return NULL;
}
/* This array is statically initialized and should always have the
* correct data */
return pdc_data[port]->port.pdc;
}
/* Note: this is defined in pdc_runtime_port_config.c when
* CONFIG_PDC_RUNTIME_PORT_CONFIG is enabled */
uint8_t pdc_power_mgmt_get_usb_pd_port_count(void)
{
return CONFIG_USB_PD_PORT_MAX_COUNT;
}
#endif /* !defined(CONFIG_PDC_RUNTIME_PORT_CONFIG) */
int pdc_power_mgmt_set_active_charge_port(int charge_port)
{
/* Note: pdc_power_mgmt_set_active_charge_port() does not alter the
* active charging port. It triggers all ports to ask charge_manager
* for the currently active port and adjust their sink path states.
*
* Overriding the active charge port externally should be done through
* charge manager's charge_manager_set_override() function.
*/
LOG_INF("%s: charge_port=%d", __func__, charge_port);
/* Contact all ports by raising a policy flag. The individual port
* state machines will react by checking if they are now the active
* charge port and adjust their sink paths accordingly.
*/
for (int i = 0; i < pdc_power_mgmt_get_usb_pd_port_count(); i++) {
atomic_set_bit(pdc_data[i]->port.snk_policy.flags,
SNK_POLICY_SET_ACTIVE_CHARGE_PORT);
k_event_post(&pdc_data[i]->port.sm_event, PDC_SM_EVENT);
}
return EC_SUCCESS;
}
void pdc_power_mgmt_check_pr_swap_needed(int port)
{
atomic_set_bit(pdc_data[port]->port.snk_policy.flags,
SNK_POLICY_EVAL_SWAP_TO_SRC);
}
int pdc_power_mgmt_set_new_power_request(int port)
{
/* Make sure port is sink connected */
if (!pdc_power_mgmt_is_sink_connected(port)) {
return -ENOTCONN;
}
LOG_INF("C%d: New power request", port);
atomic_set_bit(pdc_data[port]->port.snk_policy.flags,
SNK_POLICY_NEW_POWER_REQUEST);
k_event_post(&pdc_data[port]->port.sm_event, PDC_SM_EVENT);
return EC_SUCCESS;
}
uint8_t pdc_power_mgmt_get_task_state(int port)
{
enum pdc_state_t indicated_state, actual_state;
if (port < 0 || port >= CONFIG_USB_PD_PORT_MAX_COUNT) {
/* Ports outside of this range are never valid and would exceed
* the bounds of `pdc_data`. */
return PDC_INVALID;
}
actual_state = get_pdc_state(&pdc_data[port]->port);
switch (actual_state) {
case PDC_SEND_CMD_START:
case PDC_SEND_CMD_WAIT:
indicated_state = pdc_data[port]->port.send_cmd_return_state;
break;
default:
indicated_state = actual_state;
}
return indicated_state;
}
int pdc_power_mgmt_comm_is_enabled(int port)
{
if (pdc_power_mgmt_is_sink_connected(port) ||
pdc_power_mgmt_is_source_connected(port)) {
return true;
}
return false;
}
test_mockable bool pdc_power_mgmt_get_vconn_state(int port)
{
bool vconn_sourcing;
/* Make sure port is source connected */
if (!pdc_power_mgmt_is_source_connected(port)) {
return false;
}
pdc_data[port]->port.public_api_buff = (uint8_t *)&vconn_sourcing;
/* Block until command completes */
if (public_api_block(port, CMD_PDC_IS_VCONN_SOURCING)) {
/* something went wrong */
pdc_data[port]->port.public_api_buff = NULL;
return false;
}
pdc_data[port]->port.public_api_buff = NULL;
return vconn_sourcing;
}
bool pdc_power_mgmt_get_partner_usb_comm_capable(int port)
{
/* Make sure port is connected */
if (!pdc_power_mgmt_is_connected(port)) {
return false;
}
return pdc_data[port]->port.ccaps.op_mode_usb2 |
pdc_data[port]->port.ccaps.op_mode_usb3 |
pdc_data[port]->port.ccaps.ext_op_mode_usb4_gen2 |
pdc_data[port]->port.ccaps.ext_op_mode_usb4_gen3 |
pdc_data[port]->port.ccaps.ext_op_mode_usb4_gen4;
}
bool pdc_power_mgmt_get_partner_unconstr_power(int port)
{
/* Make sure port is sink connected */
if (!pdc_power_mgmt_is_sink_connected(port)) {
return false;
}
/* Only the fixed 5V PDO at index 0 has the UP and DRP bits set */
uint32_t vsafe_5v_pdo =
pdc_data[port]->port.snk_policy.partner_src_pdos.pdos[0];
return (vsafe_5v_pdo & PDO_FIXED_GET_UNCONSTRAINED_PWR);
}
static int pdc_power_mgmt_request_data_swap_intern(int port,
enum pd_data_role role)
{
/* Make sure port is connected */
if (!pdc_power_mgmt_is_connected(port)) {
return 1;
}
if (role == PD_ROLE_UFP) {
/* Attempt to swap to UFP */
pdc_data[port]->port.uor = (union uor_t){
.swap_to_dfp = 0,
.swap_to_ufp = 1,
/* Allow external swaps back to DFP */
.accept_dr_swap = 1,
};
} else if (role == PD_ROLE_DFP) {
/* Attempt to swap to DFP */
pdc_data[port]->port.uor = (union uor_t){
.swap_to_dfp = 1,
.swap_to_ufp = 0,
/* Prefer staying in DFP */
.accept_dr_swap = 0,
};
} else {
return EC_SUCCESS;
}
/* Block until command completes */
if (public_api_block(port, CMD_PDC_SET_UOR)) {
/* something went wrong */
return 1;
}
return EC_SUCCESS;
}
test_mockable void pdc_power_mgmt_request_data_swap(int port)
{
enum pd_data_role data_role = pdc_power_mgmt_pd_get_data_role(port);
switch (data_role) {
case PD_ROLE_DFP:
LOG_INF("C%d: Attempt data role swap from DFP to UFP", port);
pdc_power_mgmt_request_data_swap_intern(port, PD_ROLE_UFP);
break;
case PD_ROLE_UFP:
LOG_INF("C%d: Attempt data role swap from UFP to DFP", port);
pdc_power_mgmt_request_data_swap_intern(port, PD_ROLE_DFP);
break;
default:
LOG_ERR("C%d: Cannot data role swap: port is disconnected or invalid",
port);
}
}
test_mockable void pdc_power_mgmt_request_power_swap(int port)
{
if (pdc_power_mgmt_is_sink_connected(port)) {
LOG_INF("C%d: Request power role swap to source", port);
atomic_set_bit(pdc_data[port]->port.snk_policy.flags,
SNK_POLICY_SWAP_TO_SRC);
k_event_post(&pdc_data[port]->port.sm_event, PDC_SM_EVENT);
} else if (pdc_power_mgmt_is_source_connected(port)) {
LOG_INF("C%d: Request power role swap to sink", port);
atomic_set_bit(pdc_data[port]->port.src_policy.flags,
SRC_POLICY_SWAP_TO_SNK);
k_event_post(&pdc_data[port]->port.sm_event, PDC_SM_EVENT);
} else {
LOG_ERR("C%d: Cannot swap power role: "
"port not PD-attached or invalid",
port);
}
}
test_mockable enum tcpc_cc_polarity pdc_power_mgmt_pd_get_polarity(int port)
{
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return -ERANGE;
}
if (pdc_data[port]->port.connector_status.orientation) {
return POLARITY_CC2;
}
return POLARITY_CC1;
}
test_mockable enum pd_data_role pdc_power_mgmt_pd_get_data_role(int port)
{
/* Make sure port is connected */
if (!pdc_power_mgmt_is_connected(port)) {
return PD_ROLE_DISCONNECTED;
}
if (pdc_data[port]->port.connector_status.conn_partner_type ==
DFP_ATTACHED) {
return PD_ROLE_UFP;
}
return PD_ROLE_DFP;
}
test_mockable enum pd_power_role pdc_power_mgmt_get_power_role(int port)
{
/* Make sure port is connected */
if (!pdc_power_mgmt_is_connected(port)) {
return PD_ROLE_SINK;
}
if (pdc_data[port]->port.connector_status.power_direction) {
return PD_ROLE_SOURCE;
}
return PD_ROLE_SINK;
}
enum pd_cc_states pdc_power_mgmt_get_task_cc_state(int port)
{
/* Make sure port is connected */
if (!pdc_power_mgmt_is_connected(port)) {
return PD_CC_NONE;
}
switch (pdc_data[port]->port.connector_status.conn_partner_type) {
case DFP_ATTACHED:
return PD_CC_DFP_ATTACHED;
case UFP_ATTACHED:
return PD_CC_UFP_ATTACHED;
case POWERED_CABLE_NO_UFP_ATTACHED:
return PD_CC_NONE;
case POWERED_CABLE_UFP_ATTACHED:
return PD_CC_UFP_ATTACHED;
case DEBUG_ACCESSORY_ATTACHED:
return PD_CC_UFP_DEBUG_ACC;
case AUDIO_ADAPTER_ACCESSORY_ATTACHED:
return PD_CC_UFP_AUDIO_ACC;
}
return PD_CC_NONE;
}
bool pdc_power_mgmt_pd_capable(int port)
{
/* Make sure port is connected */
if (!pdc_power_mgmt_is_connected(port)) {
return false;
}
return (pdc_data[port]->port.attached_state == SNK_ATTACHED_STATE) ||
(pdc_data[port]->port.attached_state == SRC_ATTACHED_STATE);
}
bool pdc_power_mgmt_get_partner_dual_role_power(int port)
{
/* Make sure port is connected */
if (!pdc_power_mgmt_is_connected(port)) {
return false;
}
return pdc_data[port]->port.ccaps.op_mode_drp;
}
test_mockable bool pdc_power_mgmt_get_partner_data_swap_capable(int port)
{
struct pdc_port_t *pdc_port;
uint32_t fixed_vsafe5v_pdo;
/* Make sure port is connected */
if (!pdc_power_mgmt_is_connected(port)) {
return false;
}
pdc_port = &pdc_data[port]->port;
if (pdc_port->get_pdo.pdo_source != PARTNER_PDO) {
return false;
}
fixed_vsafe5v_pdo =
get_pdc_pdos_ptr(pdc_port, &pdc_port->get_pdo)->pdos[0];
/*
* Error check that first PDO is fixed, as 6.4.1 Capabilities requires
* in the Power Delivery Specification.
* "The vSafe5V Fixed Supply Object Shall always be the first object"
*/
if ((fixed_vsafe5v_pdo & PDO_TYPE_MASK) != PDO_TYPE_FIXED)
return false;
return fixed_vsafe5v_pdo & PDO_FIXED_DATA_SWAP;
}
int pdc_power_mgmt_get_vbus_voltage(int port)
{
/* Make sure port is connected */
if (!pdc_power_mgmt_is_connected(port)) {
return 0;
}
/* Return cached VBUS */
return pdc_data[port]->port.vbus;
}
test_mockable int pdc_power_mgmt_reset(int port)
{
int rv;
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return -ERANGE;
}
/* Instruct the PDC driver to reset itself. This resets the driver to
* its initial state and re-runs the PDC setup routine commands.
*/
rv = public_api_block(port, CMD_PDC_RESET);
if (rv) {
return rv;
}
/* Revert back to init state */
set_pdc_state(&pdc_data[port]->port, PDC_INIT);
return 0;
}
test_mockable uint8_t pdc_power_mgmt_get_src_cap_cnt(int port)
{
/* Make sure port is Sink connected */
if (!pdc_power_mgmt_is_sink_connected(port)) {
return 0;
}
return pdc_data[port]->port.snk_policy.partner_src_pdos.pdo_count;
}
test_mockable const uint32_t *const pdc_power_mgmt_get_src_caps(int port)
{
/* Make sure port is Sink connected */
if (!pdc_power_mgmt_is_sink_connected(port)) {
return NULL;
}
return (const uint32_t *const)pdc_data[port]
->port.snk_policy.partner_src_pdos.pdos;
}
test_mockable int pdc_power_mgmt_get_rdo(int port, uint32_t *rdo)
{
if (rdo == NULL) {
return -EINVAL;
}
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return -ERANGE;
}
/* Make sure port is sink connected and in the run sub-state */
if (!(pdc_data[port]->port.attached_state == SNK_ATTACHED_STATE &&
pdc_data[port]->port.snk_attached_local_state ==
SNK_ATTACHED_RUN)) {
return -ENODATA;
}
*rdo = pdc_data[port]->port.connector_status.rdo;
return 0;
}
static void pdc_power_mgmt_get_selected_pdo(int port, uint32_t *pdo,
uint32_t *max_ma, uint32_t *max_mv)
{
uint32_t pos = RDO_POS(pdc_data[port]->port.connector_status.rdo) - 1;
uint32_t *pdos = pdc_data[port]->port.snk_policy.partner_src_pdos.pdos;
uint32_t tmp;
*pdo = pdos[pos];
pd_extract_pdo_power(*pdo, max_ma, max_mv, &tmp);
}
uint32_t pdc_power_mgmt_get_requested_voltage(int port)
{
uint32_t pdo, max_ma, max_mv;
/* Make sure port is Sink connected and RDO is valid */
if (!pdc_power_mgmt_is_sink_connected(port) ||
!pdc_is_rdo_valid(&pdc_data[port]->port.connector_status)) {
return 0;
}
pdc_power_mgmt_get_selected_pdo(port, &pdo, &max_ma, &max_mv);
return max_mv;
}
uint32_t pdc_power_mgmt_get_requested_current(int port)
{
uint32_t pdo, max_ma, max_mv;
/* Make sure port is Sink connected and RDO is valid */
if (!pdc_power_mgmt_is_sink_connected(port) ||
!pdc_is_rdo_valid(&pdc_data[port]->port.connector_status)) {
return 0;
}
pdc_power_mgmt_get_selected_pdo(port, &pdo, &max_ma, &max_mv);
return max_ma;
}
test_mockable const char *pdc_power_mgmt_get_task_state_name(int port)
{
enum pdc_state_t state = pdc_power_mgmt_get_task_state(port);
return pdc_state_names[state];
}
test_mockable void pdc_power_mgmt_set_dual_role(int port,
enum pd_dual_role_states state)
{
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
LOG_ERR("%s called with bad port C%d.", __func__, port);
return;
}
struct pdc_port_t *port_data = &pdc_data[port]->port;
LOG_INF("C%d: pdc_power_mgmt_set_dual_role: set role to %d", port,
state);
/*
* clears the flags set in this function in case multiple consecutive
* calls to set_dual_role is made to prevent multiple roles being
* active at the same time.
*/
atomic_clear_bit(port_data->src_policy.flags, SRC_POLICY_FORCE_SNK);
atomic_clear_bit(port_data->src_policy.flags, SRC_POLICY_SWAP_TO_SNK);
atomic_clear_bit(port_data->snk_policy.flags, SNK_POLICY_SWAP_TO_SRC);
switch (state) {
/* While disconnected, toggle between src and sink */
case PD_DRP_TOGGLE_ON:
/* Allow external power role swaps */
port_data->src_policy.accept_power_role_swap = true;
port_data->snk_policy.accept_power_role_swap = true;
port_data->una_policy.cc_mode = CCOM_DRP;
port_data->drp = port_data->una_policy.drp_mode;
atomic_set_bit(port_data->una_policy.flags, UNA_POLICY_CC_MODE);
break;
/* Stay in src until disconnect, then stay in sink forever */
case PD_DRP_TOGGLE_OFF:
/* Allow external power role swap from source to sink, but not
* the reverse */
port_data->src_policy.accept_power_role_swap = true;
port_data->snk_policy.accept_power_role_swap = false;
port_data->una_policy.cc_mode = CCOM_RD;
atomic_set_bit(port_data->una_policy.flags, UNA_POLICY_CC_MODE);
break;
/* Stay in current power role, don't switch. No auto-toggle support */
case PD_DRP_FREEZE:
/* No external power role swaps accepted */
port_data->src_policy.accept_power_role_swap = false;
port_data->snk_policy.accept_power_role_swap = false;
if (pdc_power_mgmt_is_source_connected(port)) {
port_data->una_policy.cc_mode = CCOM_RP;
} else {
port_data->una_policy.cc_mode = CCOM_RD;
}
atomic_set_bit(port_data->una_policy.flags, UNA_POLICY_CC_MODE);
break;
/* Switch to sink */
case PD_DRP_FORCE_SINK:
/* Allow external power role swap from src to sink */
port_data->src_policy.accept_power_role_swap = true;
port_data->snk_policy.accept_power_role_swap = false;
if (pdc_power_mgmt_is_source_connected(port)) {
atomic_set_bit(port_data->src_policy.flags,
SRC_POLICY_SWAP_TO_SNK);
}
/*
* If PRS to Sink fails, or if not connected via PD, disconnect
* and reconnect as Sink.
*/
port_data->una_policy.cc_mode = CCOM_RD;
port_data->drp = DRP_NORMAL;
atomic_set_bit(port_data->una_policy.flags,
UNA_POLICY_DRP_MODE);
atomic_set_bit(port_data->una_policy.flags, UNA_POLICY_CC_MODE);
atomic_set_bit(port_data->src_policy.flags,
SRC_POLICY_FORCE_SNK);
break;
/* Switch to source */
case PD_DRP_FORCE_SOURCE:
/* Allow external power role swap from sink to src */
port_data->src_policy.accept_power_role_swap = false;
port_data->snk_policy.accept_power_role_swap = true;
if (pdc_power_mgmt_is_sink_connected(port)) {
atomic_set_bit(port_data->snk_policy.flags,
SNK_POLICY_SWAP_TO_SRC);
}
break;
default:
LOG_INF("C%d: Invalid dual-role state %d. Ignoring.", port,
state);
return;
}
/* Trigger updates to the power role swap allow bit */
atomic_set_bit(port_data->src_policy.flags,
SRC_POLICY_UPDATE_ALLOW_PR_SWAP);
atomic_set_bit(port_data->snk_policy.flags,
SNK_POLICY_UPDATE_ALLOW_PR_SWAP);
port_data->dual_role_state = state;
}
test_mockable enum pd_dual_role_states pdc_power_mgmt_get_dual_role(int port)
{
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return -ERANGE;
}
struct pdc_port_t *port_data = &pdc_data[port]->port;
return port_data->dual_role_state;
}
test_mockable int pdc_power_mgmt_set_trysrc(int port, bool enable)
{
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return -ERANGE;
}
LOG_INF("C%d: PD setting TrySrc=%d", port, enable);
pdc_data[port]->port.drp = (enable ? DRP_TRY_SRC : DRP_NORMAL);
return public_api_block(port, CMD_PDC_SET_DRP);
}
static void set_hpd_wake_watch(int port)
{
struct pdc_port_t *port_data = &pdc_data[port]->port;
/* Only watch for HPD wake when connected to a DP Alt Mode partner with
* HPD_LVL low.
*/
port_data->hpd_wake_watch = false;
if (!pdc_power_mgmt_pd_capable(port) ||
!(port_data->connector_status.conn_partner_flags &
CONNECTOR_PARTNER_FLAG_ALTERNATE_MODE) ||
PD_VDO_DPSTS_HPD_LVL(port_data->attention_vdo.vdo)) {
return;
}
port_data->hpd_wake_watch = true;
}
static void clear_hpd_wake_watch(int port)
{
struct pdc_port_t *port_data = &pdc_data[port]->port;
port_data->hpd_wake_watch = false;
}
/**
* PDC Chipset state Policies
*/
static void pd_chipset_resume(void)
{
k_work_reschedule(&pdc_apply_power_state_policy_work,
PDC_POWER_STATE_DEBOUNCE_MS);
LOG_INF("PD: S3->S0");
}
DECLARE_HOOK(HOOK_CHIPSET_RESUME, pd_chipset_resume, HOOK_PRIO_DEFAULT);
static void pd_chipset_suspend(void)
{
k_work_reschedule(&pdc_apply_power_state_policy_work,
PDC_POWER_STATE_DEBOUNCE_MS);
LOG_INF("PD: S0->S3");
}
DECLARE_HOOK(HOOK_CHIPSET_SUSPEND, pd_chipset_suspend, HOOK_PRIO_DEFAULT);
static void pd_chipset_startup(void)
{
k_work_reschedule(&pdc_apply_power_state_policy_work,
PDC_POWER_STATE_DEBOUNCE_MS);
LOG_INF("PD: S5->S3");
}
DECLARE_HOOK(HOOK_CHIPSET_STARTUP, pd_chipset_startup, HOOK_PRIO_DEFAULT);
static void pd_chipset_shutdown(void)
{
k_work_reschedule(&pdc_apply_power_state_policy_work,
PDC_POWER_STATE_DEBOUNCE_MS);
LOG_INF("PD: S3->S5");
}
#ifdef CONFIG_PLATFORM_EC_CHIPSET_RESUME_INIT_HOOK
DECLARE_HOOK(HOOK_CHIPSET_SHUTDOWN_COMPLETE, pd_chipset_shutdown,
HOOK_PRIO_DEFAULT);
#else
DECLARE_HOOK(HOOK_CHIPSET_SHUTDOWN, pd_chipset_shutdown, HOOK_PRIO_DEFAULT);
#endif
static void pdc_battery_status_changed(void)
{
for (int i = 0; i < pdc_power_mgmt_get_usb_pd_port_count(); i++) {
if (pdc_power_mgmt_is_pdc_port_valid(i)) {
pdc_update_battery_status(&pdc_data[i]->port, false);
}
}
}
DECLARE_HOOK(HOOK_BATTERY_SOC_CHANGE, pdc_battery_status_changed,
HOOK_PRIO_DEFAULT);
/**
* @brief Send updated battery status to the PDC
*
* @param port PDC port to update
* @param force If true, unconditionally send the battery status. Otherwise
* only send battery status if the battery information changed.
*/
static void pdc_update_battery_status(struct pdc_port_t *port, bool force)
{
const struct pdc_config_t *config = port->dev->config;
int port_number = config->connector_num;
union battery_status_t bsdo = { 0 };
if (battery_is_present() == BP_YES) {
uint32_t v;
uint32_t c;
if (battery_design_voltage(&v) != 0 ||
battery_remaining_capacity(&c) != 0) {
bsdo.present_capacity = BSDO_CAP_UNKNOWN;
} else {
/*
* Wh = (c * v) / 1000000
* 10th of a Wh = Wh * 10
*/
bsdo.present_capacity =
DIV_ROUND_NEAREST((c * v), 100000);
}
/* Battery is present */
bsdo.battery_present = 1;
/*
* For drivers that are not smart battery compliant,
* battery_status() returns EC_ERROR_UNIMPLEMENTED and
* the battery is assumed to be idle.
*/
if (battery_status(&c) != 0) {
/* Assume idle if battery status is not available. */
bsdo.battery_state = BSDO_BATTERY_STATE_IDLE;
} else {
if (c & STATUS_FULLY_CHARGED) {
/* Fully charged */
bsdo.battery_state = BSDO_BATTERY_STATE_IDLE;
} else if (c & STATUS_DISCHARGING) {
/* Discharging */
bsdo.battery_state =
BSDO_BATTERY_STATE_DISCHARGING;
} else {
/* Else battery is charging.*/
bsdo.battery_state =
BSDO_BATTERY_STATE_CHARGING;
}
}
} else {
bsdo.battery_present = 0;
bsdo.present_capacity = BSDO_CAP_UNKNOWN;
}
if (!force && memcmp(&port->bstat, &bsdo, sizeof(bsdo)) == 0) {
return;
}
port->bstat = bsdo;
if (pdc_power_mgmt_is_sink_connected(port_number)) {
atomic_set_bit(port->snk_policy.flags,
SNK_POLICY_UPDATE_BATTERY_STATUS);
} else if (pdc_power_mgmt_is_source_connected(port_number)) {
atomic_set_bit(port->src_policy.flags,
SRC_POLICY_UPDATE_BATTERY_STATUS);
}
}
static void pdc_update_battery_capability(struct pdc_port_t *port)
{
const struct pdc_config_t *config = port->dev->config;
int port_number = config->connector_num;
union battery_capability_t bcdb = { 0 };
/* TODO: Replace with proper VID:PID API */
/* Set VID */
bcdb.vid = CONFIG_PLATFORM_EC_USB_VID;
/* Set PID */
bcdb.pid = CONFIG_PLATFORM_EC_USB_PID;
if (battery_is_present() == BP_YES) {
uint32_t v;
uint32_t c;
/*
* The Battery Design Capacity field shall return the
* Battery’s design capacity in tenths of Wh. If the
* Battery is Hot Swappable and is not present, the
* Battery Design Capacity field shall be set to 0. If
* the Battery is unable to report its Design Capacity,
* it shall return 0xFFFF
*/
bcdb.design_capacity = 0xffff;
/*
* The Battery Last Full Charge Capacity field shall
* return the Battery’s last full charge capacity in
* tenths of Wh. If the Battery is Hot Swappable and
* is not present, the Battery Last Full Charge Capacity
* field shall be set to 0. If the Battery is unable to
* report its Design Capacity, the Battery Last Full
* Charge Capacity field shall be set to 0xFFFF.
*/
bcdb.last_full_charge_capacity = 0xffff;
if (battery_design_voltage(&v) == 0) {
if (battery_design_capacity(&c) == 0) {
/*
* Wh = (c * v) / 1000000
* 10th of a Wh = Wh * 10
*/
bcdb.design_capacity =
DIV_ROUND_NEAREST((c * v), 100000);
}
if (battery_full_charge_capacity(&c) == 0) {
/*
* Wh = (c * v) / 1000000
* 10th of a Wh = Wh * 10
*/
bcdb.last_full_charge_capacity =
DIV_ROUND_NEAREST((c * v), 100000);
}
}
}
port->bcap = bcdb;
if (pdc_power_mgmt_is_sink_connected(port_number)) {
atomic_set_bit(port->snk_policy.flags,
SNK_POLICY_UPDATE_BATTERY_CAPABILITY);
} else if (pdc_power_mgmt_is_source_connected(port_number)) {
atomic_set_bit(port->src_policy.flags,
SRC_POLICY_UPDATE_BATTERY_CAPABILITY);
}
}
test_mockable int pdc_power_mgmt_get_drp_mode(int port,
enum drp_mode_t *drp_mode)
{
int ret;
/* Make sure port is in range and that an output buffer is provided */
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return -ERANGE;
}
if (drp_mode == NULL) {
return -EINVAL;
}
ret = public_api_block(port, CMD_PDC_GET_DRP);
if (ret) {
return ret;
}
*drp_mode = pdc_data[port]->port.drp_read;
return 0;
}
test_mockable int pdc_power_mgmt_get_info(int port, struct pdc_info_t *pdc_info,
bool live)
{
int ret;
/* Make sure port is in range and that an output buffer is provided */
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return -ERANGE;
}
if (pdc_info == NULL) {
return -EINVAL;
}
if (live) {
/* Caller wants live chip info. Set up a public API call to
* retrieve it from the PDC.
*/
ret = public_api_block(port, CMD_PDC_GET_INFO);
if (ret) {
return ret;
}
/* Provide a copy of the current info struct to avoid exposing
* internal data structs.
*/
memcpy(pdc_info, &pdc_data[port]->port.info,
sizeof(struct pdc_info_t));
return 0;
}
/* Non-live requests can be handled synchronously by calling directly
* into the PDC driver.
*/
return pdc_get_info(pdc_data[port]->port.pdc, pdc_info, false);
}
test_mockable int pdc_power_mgmt_get_lpm_ppm_info(int port,
struct lpm_ppm_info_t *info)
{
int ret;
/* Make sure port is in range and that an output buffer is provided */
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return -ERANGE;
}
if (info == NULL) {
return -EINVAL;
}
pdc_data[port]->port.lpm_ppm_info = info;
ret = public_api_block(port, CMD_PDC_GET_LPM_PPM_INFO);
if (ret) {
return ret;
}
return 0;
}
int pdc_power_mgmt_get_hw_config(int port,
struct pdc_hw_config_t *pdc_hw_config)
{
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return -ERANGE;
}
/* This operation is handled synchronously within the driver based on
* compile-time data. No need to block or go through the state machine.
*/
return pdc_get_hw_config(pdc_data[port]->port.pdc, pdc_hw_config);
}
int pdc_power_mgmt_get_rev(int port, enum tcpci_msg_type type)
{
uint32_t rev;
/* Make sure port is connected */
if (!pdc_power_mgmt_is_connected(port)) {
return 0;
}
switch (type) {
case TCPCI_MSG_SOP:
rev = pdc_data[port]->port.ccaps.partner_pd_revision;
break;
case TCPCI_MSG_SOP_PRIME:
rev = pdc_data[port]->port.cable_prop.cable_pd_revision;
break;
default:
rev = 0;
}
return rev;
}
const uint32_t *const pdc_power_mgmt_get_snk_caps(int port)
{
/* Make sure port is Sink connected */
if (!pdc_power_mgmt_is_source_connected(port)) {
return NULL;
}
return (const uint32_t *const)pdc_data[port]
->port.src_policy.partner_snk_pdos.pdos;
}
uint8_t pdc_power_mgmt_get_snk_cap_cnt(int port)
{
/* Make sure port is Sink connected */
if (!pdc_power_mgmt_is_source_connected(port)) {
return 0;
}
return pdc_data[port]->port.src_policy.partner_snk_pdos.pdo_count;
}
struct rmdo pdc_power_mgmt_get_partner_rmdo(int port)
{
struct rmdo value = { 0 };
/* The PD 3.1 Get_Revision Message is optional and currently not
* supported in the PDC, although this may change in future updates. */
return value;
}
enum pd_discovery_state
pdc_power_mgmt_get_identity_discovery(int port, enum tcpci_msg_type type)
{
enum pdc_cmd_t cmd;
int ret;
/* Make sure port is Sink connected */
if (!pdc_power_mgmt_is_connected(port)) {
return PD_DISC_NEEDED;
}
switch (type) {
case TCPCI_MSG_SOP:
cmd = CMD_PDC_GET_IDENTITY_DISCOVERY;
break;
case TCPCI_MSG_SOP_PRIME:
return (pdc_data[port]->port.cable_prop.cable_type &&
pdc_data[port]->port.cable_prop.mode_support) ?
PD_DISC_COMPLETE :
PD_DISC_FAIL;
default:
return PD_DISC_FAIL;
}
/* Block until command completes */
ret = public_api_block(port, cmd);
if (ret) {
return PD_DISC_NEEDED;
}
return pdc_data[port]->port.discovery_state ? PD_DISC_COMPLETE :
PD_DISC_FAIL;
}
test_mockable int
pdc_power_mgmt_connector_reset(int port, enum connector_reset reset_type)
{
/* Make sure port is in range and that an output buffer is provided */
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return -ERANGE;
}
/* Make sure port is connected */
if (!pdc_power_mgmt_is_connected(port)) {
return EC_SUCCESS;
}
pdc_data[port]->port.connector_reset.raw_value = 0;
pdc_data[port]->port.connector_reset.reset_type = reset_type;
/* Block until command completes */
return public_api_block(port, CMD_PDC_CONNECTOR_RESET);
}
static int pdc_run_get_discovery(int port)
{
int ret;
/* Make sure port is in range and that an output buffer is provided */
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return -ERANGE;
}
/* Make sure port is connected and PD capable */
if (!pdc_power_mgmt_is_connected(port) ||
!pdc_power_mgmt_pd_capable(port)) {
return 0;
}
/* Format the GET_VDO command */
discovery_info_init(&pdc_data[port]->port);
/* Block until command completes */
ret = public_api_block(port, CMD_PDC_GET_VDO);
if (ret) {
return ret;
}
LOG_INF("GET_VDO[%d]: vid = %04x, pid = %04x, prod_type = %d", port,
PD_IDH_VID(pdc_data[port]->port.vdo[0]),
PD_PRODUCT_PID(pdc_data[port]->port.vdo[1]),
PD_IDH_PTYPE(pdc_data[port]->port.vdo[0]));
return 0;
}
uint16_t pdc_power_mgmt_get_identity_vid(int port)
{
uint16_t vid = 0;
struct pdc_port_t *pdc;
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return vid;
}
pdc = &pdc_data[port]->port;
/*
* TODO(b/327283662); GET_VDO completes with 0 length bytes to read
*
* The VDOs should be retrieved as part of either the src_attached or
* snk_attached state flows. However, if the port is connected during an
* EC reboot, then the GET_VDO command will complete successfully, but
* indicates a 0 VDO length and so the ST_READ state is skipped in the
* driver. Adding a work-around here such that if the first VDO is all
* 0s, then trigger another GET_VDO command in order to get the values
* required. GET_VDO is only sent, if the port is connected and pd
* capable.
*
*/
if (pdc->vdo[IDENTITY_VID_VDO_IDX] == INVALID_VDO_VALUE) {
pdc_run_get_discovery(port);
}
if (pdc->vdo[IDENTITY_VID_VDO_IDX]) {
vid = PD_IDH_VID(pdc->vdo[IDENTITY_VID_VDO_IDX]);
}
return vid;
}
uint16_t pdc_power_mgmt_get_identity_pid(int port)
{
uint16_t pid = 0;
struct pdc_port_t *pdc;
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return pid;
}
pdc = &pdc_data[port]->port;
if (pdc->vdo[IDENTITY_VID_VDO_IDX] == INVALID_VDO_VALUE) {
pdc_run_get_discovery(port);
}
if (pdc->vdo[IDENTITY_PID_VDO_IDX]) {
pid = PD_PRODUCT_PID(pdc->vdo[IDENTITY_PID_VDO_IDX]);
}
return pid;
}
uint8_t pdc_power_mgmt_get_product_type(int port)
{
uint8_t ptype = 0;
struct pdc_port_t *pdc;
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return ptype;
}
pdc = &pdc_data[port]->port;
if (pdc->vdo[IDENTITY_PTYPE_VDO_IDX] == INVALID_VDO_VALUE) {
pdc_run_get_discovery(port);
}
if (pdc->vdo[IDENTITY_PTYPE_VDO_IDX]) {
ptype = PD_IDH_PTYPE(pdc->vdo[IDENTITY_PTYPE_VDO_IDX]);
}
return ptype;
}
/** Allow 3s for the PDC SM to suspend itself. */
#define SUSPEND_TIMEOUT_USEC (3 * USEC_PER_SEC)
#ifdef CONFIG_USBC_PDC_DRIVEN_CCD
/** Store the CCD port's SBU mux operating mode when suspending */
static enum pdc_sbu_mux_mode sbu_mux_mode_at_suspend = PDC_SBU_MUX_MODE_NORMAL;
#endif /* CONFIG_USBC_PDC_DRIVEN_CCD */
/* TODO(b/323371550): These functions should be adjusted to target individual PD
* chips rather than all ports at once. It should take a chip ID as a param and
* track current comms status by chip.
*/
/**
* @brief Suspend/disable communication to the PDC
*/
static int suspend_pdc_comms(void)
{
int ret;
int status = 0;
uint8_t port_count = pdc_power_mgmt_get_usb_pd_port_count();
#ifdef CONFIG_USBC_PDC_DRIVEN_CCD
/* Save current SBU mux override state so we can restore upon
* PDC subsystem resume. */
ret = pdc_power_mgmt_get_sbu_mux_mode(&sbu_mux_mode_at_suspend, NULL);
if (ret) {
LOG_ERR("PD: Cannot read current SBU mux mode: %d", ret);
} else {
LOG_INF("PD: Save SBU mux mode of %d", sbu_mux_mode_at_suspend);
}
#endif /* CONFIG_USBC_PDC_DRIVEN_CCD */
/* Request each port's PDC state machine to enter the suspend
* state.
*/
for (int p = 0; p < port_count; p++) {
atomic_set(&pdc_data[p]->port.suspend, 1);
}
/* Wait for each PDC state machine to enter suspended state */
for (int p = 0; p < port_count; p++) {
if (get_pdc_state(&pdc_data[p]->port) == PDC_DISABLED) {
/* Ignore disabled ports */
continue;
}
ret = WAIT_FOR(
get_pdc_state(&pdc_data[p]->port) == PDC_SUSPENDED,
SUSPEND_TIMEOUT_USEC, k_sleep(K_MSEC(LOOP_DELAY_MS)));
if (!ret) {
LOG_ERR("Timed out suspending PDC SM for port "
"C%d: %d",
p, ret);
status = -ETIMEDOUT;
}
}
/* Suspend the driver layer */
for (int p = 0; p < port_count; p++) {
if (get_pdc_state(&pdc_data[p]->port) == PDC_DISABLED) {
/* Ignore disabled ports */
continue;
}
ret = pdc_set_comms_state(pdc_data[p]->port.pdc, false);
if (ret) {
LOG_ERR("Cannot suspend port C%d driver: %d", p, ret);
status = ret;
}
}
return status;
}
/**
* @brief Resume/enable communication to the PDC
*/
static int resume_pdc_comms(void)
{
int ret;
int status = 0;
uint8_t port_count = pdc_power_mgmt_get_usb_pd_port_count();
#ifdef CONFIG_USBC_PDC_DRIVEN_CCD
if (sbu_mux_mode_at_suspend == PDC_SBU_MUX_MODE_FORCE_DBG) {
/* Set a flag on the CCD port to go back into force-debug mode.
* This cannot be done immediately because the drivers need time
* to re-initialize. If the previous SBU mux mode was normal
* operation, that is the default and no action is necessary.
*/
int ccd_port = pdc_power_mgmt_get_ccd_port();
LOG_INF("PD: Restore C%d (CCD) SBU mux to forced-debug mode",
ccd_port);
atomic_set_bit(pdc_data[ccd_port]->port.common_policy.flags,
COMMON_POLICY_SET_SBU_MUX_TO_FORCED_DEBUG);
}
#endif /* CONFIG_USBC_PDC_DRIVEN_CCD */
/* Resume and reset the driver layer */
for (int p = 0; p < port_count; p++) {
if (get_pdc_state(&pdc_data[p]->port) == PDC_DISABLED) {
/* Ignore disabled ports */
continue;
}
ret = pdc_set_comms_state(pdc_data[p]->port.pdc, true);
if (ret) {
LOG_ERR("Cannot resume port C%d driver: %d", p, ret);
status = ret;
}
}
/* Release each PDC state machine. A reset is performed when
* exiting the suspended state.
*/
for (int p = 0; p < port_count; p++) {
atomic_set(&pdc_data[p]->port.suspend, 0);
}
return status;
}
test_mockable int pdc_power_mgmt_set_comms_state(bool enable_comms)
{
int ret;
static bool current_comms_status = true;
if (enable_comms == current_comms_status) {
LOG_ERR("PD: Unnecessary suspend or resume. "
"Current state already %d",
current_comms_status);
return -EALREADY;
}
if (enable_comms) {
LOG_INF("PD: Resume PDC communication");
ret = resume_pdc_comms();
} else {
LOG_INF("PD: Suspend PDC communication");
ret = suspend_pdc_comms();
}
if (ret == 0) {
/* Successfully changed comms state */
current_comms_status = enable_comms;
}
return ret;
}
test_mockable int
pdc_power_mgmt_get_connector_status(int port,
union connector_status_t *connector_status)
{
struct pdc_port_t *pdc;
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return -ERANGE;
}
if (connector_status == NULL) {
return -EINVAL;
}
pdc = &pdc_data[port]->port;
*connector_status = pdc->connector_status;
return 0;
}
test_mockable int pdc_power_mgmt_get_last_status_change(
int port, union conn_status_change_bits_t *status_change)
{
struct pdc_port_t *pdc;
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return -ERANGE;
}
if (status_change == NULL) {
return -EINVAL;
}
pdc = &pdc_data[port]->port;
status_change->raw_value = pdc->ci.raw_value;
return 0;
}
#ifdef CONFIG_PLATFORM_EC_USB_PD_DP_MODE
uint8_t pdc_power_mgmt_get_dp_pin_mode(int port)
{
uint8_t pin_mode;
/* Make sure port is in range and that an output buffer is provided */
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
LOG_ERR("get_dp_pin_mode: invalid port %d", port);
return 0;
}
/* Make sure port is connected and PD capable */
if (!pdc_power_mgmt_is_connected(port)) {
return 0;
}
/* Check if Alternate Mode is actually active */
if (!(pdc_data[port]->port.connector_status.conn_partner_flags &
CONNECTOR_PARTNER_FLAG_ALTERNATE_MODE)) {
return 0;
}
/*
* Byte 1 (bits 15:8) contains the DP Source Device Pin assignment.
* The VDO pin assignments match our MODE_DP_PIN_x definitions.
*/
pin_mode = (pdc_data[port]->port.vdo_dp_cfg >> 8) & 0xFF;
LOG_INF("C%d: DP pin mode 0x%02x", port, pin_mode);
return pin_mode;
}
mux_state_t pdc_power_mgmt_get_dp_mux_mode(int port)
{
int pin_mode = get_dp_pin_mode(port);
/* Default dp_port_mf_allow is true */
int mf_pref = PD_VDO_DPSTS_MF_PREF(pdc_power_mgmt_get_dp_status(port));
/*
* Multi-function operation is only allowed if that pin config is
* supported.
*/
if ((pin_mode & MODE_DP_PIN_MF_MASK) && mf_pref)
return USB_PD_MUX_DOCK;
else
return USB_PD_MUX_DP_ENABLED;
}
#endif
void pdc_power_mgmt_set_max_voltage(unsigned int mv)
{
unsigned int max_mw;
if (mv < PD_MIN_MV || mv > CONFIG_PLATFORM_EC_USB_PD_MAX_VOLTAGE_MV) {
LOG_ERR("PD: Ignore invalid voltage request of %umV "
"(allowed range %u-%umV)",
mv, PD_MIN_MV,
CONFIG_PLATFORM_EC_USB_PD_MAX_VOLTAGE_MV);
return;
}
LOG_INF("PD: New maximum voltage: %dmV", mv);
pdc_max_request_mv = mv;
/* Adjust our sink PDOs to the new maximum voltage and adjust the max
* power so we don't exceed the board current limit.
*/
max_mw = pdc_max_request_mv * CONFIG_PLATFORM_EC_USB_PD_MAX_CURRENT_MA /
1000;
pdc_snk_pdos.pdos[SNK_PDO_BATT_POS] =
PDO_BATT(4750, pdc_max_request_mv, max_mw);
pdc_snk_pdos.pdos[SNK_PDO_VAR_POS] =
PDO_VAR(4750, pdc_max_request_mv,
CONFIG_PLATFORM_EC_USB_PD_MAX_CURRENT_MA);
/* All ports need to set new SINK PDOs */
for (int i = 0; i < pdc_power_mgmt_get_usb_pd_port_count(); i++) {
atomic_set_bit(pdc_data[i]->port.cci_flags, CCI_SET_SINK_PDOS);
}
}
test_mockable unsigned int pdc_power_mgmt_get_max_voltage(void)
{
return pdc_max_request_mv;
}
test_mockable void pdc_power_mgmt_request_source_voltage(int port, int mv)
{
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
LOG_ERR("%s: Invalid port parameter %d", __func__, port);
return;
}
pdc_power_mgmt_set_max_voltage(mv);
/* pdc_power_mgmt_set_max_voltage() triggers a SET_PDOS command
* to set new SINK PDOs for the LPM.
*
* If we are SNK_ATTACHED, trigger a re-evaluation of the SRC CAPS
* based on the new voltage limit. This generates a SET_RDO message
* to initiate a new Request message with the partner and set a new
* contract.
*
* TODO: b/494687197 - remove the call to
* pdc_power_mgmt_set_new_power_request() once all PDCs are verified
* to automatically negotiate a new contract when the EC sets new
* SINK PDOs in the LPM.
*/
if (pdc_power_mgmt_is_sink_connected(port)) {
pdc_power_mgmt_set_new_power_request(port);
} else if (pdc_power_mgmt_is_source_connected(port)) {
/* We are a source, swap to sink. The CCI_SET_SINK_PDOS
* will be handled prior to the check of the source policy
* flags, which ensures the new sink PDOs are in place prior
* the the power swap to sink.
*/
LOG_INF("C%d: Swapping to sink role to request new "
"source voltage",
port);
pdc_power_mgmt_request_power_swap(port);
}
/* For all other states (non-PD and unattached), the next time
* we connect as SNK_ATTACHED, the PDC will use most recent SINK PDOS.
*/
}
test_mockable int
pdc_power_mgmt_get_cable_prop(int port, union cable_property_t *cable_prop)
{
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return -ERANGE;
}
if (cable_prop == NULL) {
return -EINVAL;
}
*cable_prop = pdc_data[port]->port.cable_prop;
return 0;
}
void pdc_power_mgmt_request_tbt_reset(int port_num)
{
struct pdc_port_t *port;
if (!pdc_power_mgmt_is_pdc_port_valid(port_num)) {
return;
}
port = &pdc_data[port_num]->port;
if (!atomic_test_bit(port->src_policy.flags, SRC_POLICY_TBT_RESET)) {
atomic_set_bit(port->src_policy.flags, SRC_POLICY_TBT_RESET);
k_event_post(&port->sm_event, PDC_SM_EVENT);
}
}
__overridable enum usb_typec_current_t
pdc_power_mgmt_get_default_current_limit(int port)
{
return TC_CURRENT_1_5A;
}
/**
* @brief Adjust typec and USB-PD current limits
*/
int pdc_power_mgmt_set_current_limit(int port_num,
enum usb_typec_current_t current)
{
struct pdc_port_t *pdc;
if (!pdc_power_mgmt_is_pdc_port_valid(port_num)) {
return -ERANGE;
}
pdc = &pdc_data[port_num]->port;
/* Always set the new Rp value */
pdc->una_policy.tcc = current;
/* Always set the new SRC PDO. */
pdc->src_policy.lpm_src_pdo = current == TC_CURRENT_3_0A ?
pdc_src_pdo_max :
pdc_src_pdo_nominal;
LOG_INF("C%d: set current limit %s", port_num,
(current == TC_CURRENT_3_0A) ? "3.0A" : "1.5A");
/* Further actions depend on the port attached state and power role */
switch (pdc->attached_state) {
case INIT_STATE:
break;
case SRC_ATTACHED_TYPEC_ONLY_STATE:
/*
* Active TypeC only SRC connection. Because the connection is
* active and not a PD connection, apply the new Rp value now.
*/
atomic_set_bit(pdc->common_policy.flags, COMMON_POLICY_SET_RP);
__fallthrough;
case SRC_ATTACHED_STATE:
/*
* Active USB-PD SRC connection. Update the LPM source cap which
* will also trigger the PDC to send a new SRC_CAP message to
* the port partner.
*/
/* Set flag to trigger SET_PDOS command to PDC */
atomic_set_bit(pdc->src_policy.flags,
SRC_POLICY_UPDATE_SRC_CAPS);
break;
case SNK_ATTACHED_STATE:
/*
* Src policy can be set in a Snk connection to support an FRS
* partner. Update RP accordingly.
*/
atomic_set_bit(pdc->common_policy.flags, COMMON_POLICY_SET_RP);
__fallthrough;
case SNK_ATTACHED_TYPEC_ONLY_STATE:
/* Even when operating as a SNK, update the SRC caps
* so that the first PDO offered after a power role
* swap is a safe value.
*/
atomic_set_bit(pdc->snk_policy.flags,
SNK_POLICY_UPDATE_SRC_CAPS);
break;
case UNATTACHED_STATE:
/* Update the default Rp level */
atomic_set_bit(pdc->una_policy.flags, UNA_POLICY_TCC);
/* Set flag to trigger SET_PDOS command to PDC */
atomic_set_bit(pdc->una_policy.flags,
UNA_POLICY_UPDATE_SRC_CAPS);
break;
}
return EC_SUCCESS;
}
bool pdc_power_mgmt_get_frs_hw_supported(int port)
{
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
LOG_ERR("get_frs_hw_supported: invalid port %d", port);
return false;
}
return pdc_get_frs_supported(pdc_data[port]->port.pdc);
}
test_mockable int pdc_power_mgmt_get_pch_data_status(int port, uint8_t *status)
{
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return -ERANGE;
}
if (status == NULL) {
return -EINVAL;
}
/* Block until command completes */
if (public_api_block(port, CMD_PDC_GET_PCH_DATA_STATUS)) {
/* something went wrong */
return -EIO;
}
memcpy(status, pdc_data[port]->port.pch_data_status, 5);
return 0;
}
int pdc_power_mgmt_wait_for_sync(int port, int timeout_ms)
{
struct pdc_port_t *pdc;
int rv;
int ktime = (timeout_ms == -1 ? PDC_SM_SETTLED_TIMEOUT_MS : timeout_ms);
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return -ERANGE;
}
pdc = &pdc_data[port]->port;
/* Trigger re-scan of connector status. */
atomic_set_bit(pdc->cci_flags, CCI_EVENT);
k_event_post(&pdc->sm_event, PDC_SM_EVENT);
/* To avoid any race conditions, set reset arg to true to clear all
* events in settle_event, then wait for PDC_SM_SETTLED_EVENT to be
* posted. This is all handled under a lock within k_event_wait. */
rv = k_event_wait(&pdc->settle_event, PDC_SM_SETTLED_EVENT, true,
K_MSEC(ktime));
if (!rv) {
return -ETIMEDOUT;
}
k_event_clear(&pdc->settle_event, rv);
return 0;
}
int pdc_power_mgmt_ppm_ack_status_change(int port,
union conn_status_change_bits_t ci)
{
struct pdc_port_t *pdc;
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return -ERANGE;
}
pdc = &pdc_data[port]->port;
pdc->overlay_ppm_changes.raw_value &= ~(ci.raw_value);
pdc->connector_status.raw_conn_status_change_bits &= ~(ci.raw_value);
return 0;
}
int pdc_power_mgmt_register_ppm_callback(const struct pdc_callback *callback)
{
struct pdc_port_t *pdc;
int port;
/* The callback can safely be applied to all port structs, even inactive
* ones. Use the CONFIG_USB_PD_PORT_MAX_COUNT constant instead of
* pdc_power_mgmt_get_usb_pd_port_count() to make this function safe to
* call before initialization is complete. */
for (port = 0; port < CONFIG_USB_PD_PORT_MAX_COUNT; ++port) {
pdc = &pdc_data[port]->port;
pdc->ppm_ci_cb = callback;
}
return 0;
}
int pdc_power_mgmt_register_board_callback(enum pdc_power_mgmt_board_cb_t type,
const void *callback)
{
struct pdc_port_t *pdc;
int port;
/* The callback can safely be applied to all port structs, even inactive
* ones. Use the CONFIG_USB_PD_PORT_MAX_COUNT constant instead of
* pdc_power_mgmt_get_usb_pd_port_count() to make this function safe to
* call before initialization is complete. */
for (port = 0; port < CONFIG_USB_PD_PORT_MAX_COUNT; ++port) {
pdc = &pdc_data[port]->port;
switch (type) {
case PDC_BOARD_CB_UNATTACH:
pdc->board_unattach_cb =
(pdc_power_mgmt_board_unattached_cb)callback;
break;
case PDC_BOARD_CB_DP_ATTENTION:
pdc->board_dp_attention_cb =
(pdc_power_mgmt_board_dp_attention_cb)callback;
break;
default:
break;
};
}
return 0;
}
int pdc_power_mgmt_get_connector_status_for_ppm(
int port, union connector_status_t *connector_status)
{
struct pdc_port_t *pdc;
int rv;
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return -ERANGE;
}
pdc = &pdc_data[port]->port;
/* If a PPM event is pending, we need to wait for the connector status
* ready event to be set before continuing.
*/
if (atomic_test_bit(pdc->cci_flags, CCI_PPM_EVENT)) {
rv = k_event_wait(&pdc->settle_event,
PDC_PPM_CONNECTOR_STATUS_READY, false,
K_MSEC(PDC_SM_SETTLED_TIMEOUT_MS));
if (!rv) {
return -ETIMEDOUT;
}
k_event_clear(&pdc->settle_event, rv);
}
rv = pdc_power_mgmt_get_connector_status(port, connector_status);
/* Overlay any additional connector status change bits we would like to
* add. This is necessary for the OPM to be made aware of role swaps and
* other methods causing connector status changes.
*/
if (rv == 0) {
connector_status->raw_conn_status_change_bits |=
pdc->overlay_ppm_changes.raw_value;
}
return rv;
}
#ifdef CONFIG_USBC_PDC_DRIVEN_CCD
int pdc_power_mgmt_get_ccd_port(void)
{
struct pdc_hw_config_t pdc_hw_config;
for (int i = 0; i < pdc_power_mgmt_get_usb_pd_port_count(); i++) {
const struct pdc_config_t *cfg = pdc_data[i]->port.dev->config;
pdc_power_mgmt_get_hw_config(i, &pdc_hw_config);
if (pdc_hw_config.ccd) {
return cfg->connector_num;
}
}
/* This is unreachable since compile-time checks in the PDC driver
* sources ensure that one port (per PDC type) is tagged as CCD when
* CONFIG_USBC_PDC_DRIVEN_CCD is enabled. */
return -1; /* LCOV_EXCL_LINE */
}
test_mockable int pdc_power_mgmt_get_sbu_mux_mode(enum pdc_sbu_mux_mode *mode,
int *port_num)
{
if (mode == NULL) {
return -EINVAL;
}
int port = pdc_power_mgmt_get_ccd_port();
__ASSERT(port >= 0, "No CCD port specified in devicetree");
if (port_num) {
*port_num = port;
}
/* Block until command completes */
if (public_api_block(port, CMD_PDC_GET_SBU_MUX_MODE)) {
return -EIO;
}
*mode = pdc_data[port]->port.sbu_mux_mode;
return 0;
}
test_mockable int pdc_power_mgmt_set_sbu_mux_mode(enum pdc_sbu_mux_mode mode)
{
int port = pdc_power_mgmt_get_ccd_port();
__ASSERT(port >= 0, "No CCD port specified in devicetree");
pdc_data[port]->port.sbu_mux_mode = mode;
LOG_INF("C%d: Setting SBU mux mode to %d", port, mode);
/* Block until command completes */
return public_api_block(port, CMD_PDC_SET_SBU_MUX_MODE);
}
#endif /* defined(CONFIG_USBC_PDC_DRIVEN_CCD) */
test_mockable int pdc_power_mgmt_set_bbr_cts(int port, bool enable)
{
/* Make sure port is in range and that an output buffer is provided */
if (!pdc_power_mgmt_is_pdc_port_valid(port)) {
return -ERANGE;
}
pdc_data[port]->port.bbr_cts_enable = enable;
/* Block until command completes */
return public_api_block(port, CMD_PDC_SET_BBR_CTS);
}
test_mockable int pdc_power_mgmt_set_ap_power_state(enum power_state state)
{
if (!(state == POWER_S0 || state == POWER_S5)) {
LOG_ERR("PD: Can only notify S0 and S5 states (state=%d)",
state);
return -EINVAL;
}
for (int i = 0; i < pdc_power_mgmt_get_usb_pd_port_count(); i++) {
/* Issue command on all port instances */
pdc_notify_ap_power_state(i, state);
}
return 0;
}
test_mockable void pdc_power_mgmt_simulate_power_button_press(int ms)
{
if (!IS_ENABLED(CONFIG_PLATFORM_EC_POWER_BUTTON))
return;
power_button_simulate_press(ms);
}
#ifdef CONFIG_ZTEST
bool test_pdc_power_mgmt_is_snk_typec_attached_run(int port)
{
__ASSERT(pdc_power_mgmt_is_pdc_port_valid(port), "Invalid USB-C port");
LOG_INF("RPZ SRC %d",
pdc_data[port]->port.snk_typec_attached_local_state);
return pdc_data[port]->port.snk_typec_attached_local_state ==
SNK_TYPEC_ATTACHED_RUN;
}
bool test_pdc_power_mgmt_is_src_typec_attached_run(int port)
{
__ASSERT(pdc_power_mgmt_is_pdc_port_valid(port), "Invalid USB-C port");
LOG_INF("RPZ SRC %d",
pdc_data[port]->port.src_typec_attached_local_state);
return pdc_data[port]->port.src_typec_attached_local_state ==
SRC_TYPEC_ATTACHED_RUN;
}
/*
* Reset the state machine for each port to its unattached state. This ensures
* that tests start from the same state and prevents commands from a previous
* test from impacting subsequently run tests.
*/
/* LCOV_EXCL_START */
bool pdc_power_mgmt_test_wait_unattached(void)
{
int num_unattached;
for (int port = 0; port < ARRAY_SIZE(pdc_data); port++) {
set_pdc_state(&pdc_data[port]->port, PDC_UNATTACHED);
}
/* Wait for up to 20 * 100ms for all ports to become unattached. */
for (int i = 0; i < 20; i++) {
k_msleep(100);
num_unattached = 0;
for (int port = 0; port < ARRAY_SIZE(pdc_data); port++) {
if (pdc_data[port]->port.unattached_local_state ==
UNATTACHED_RUN) {
num_unattached++;
}
}
if (num_unattached == ARRAY_SIZE(pdc_data)) {
return true;
}
}
return false;
}
/* LCOV_EXCL_STOP */
/*
* Ensure that the PDC attached state is either SRC_ATTACHED or SNK_ATTACHED and
* that the substate has reached the stead state for the attached state.
*/
/* LCOV_EXCL_START */
bool pdc_power_mgmt_is_pd_attached(int port)
{
__ASSERT(pdc_power_mgmt_is_pdc_port_valid(port), "Invalid USB-C port");
if ((pdc_data[port]->port.attached_state == SNK_ATTACHED_STATE) &&
(pdc_data[port]->port.snk_attached_local_state ==
SNK_ATTACHED_RUN)) {
return true;
}
if ((pdc_data[port]->port.attached_state == SRC_ATTACHED_STATE) &&
(pdc_data[port]->port.src_attached_local_state ==
SRC_ATTACHED_RUN)) {
return true;
}
return false;
}
/* LCOV_EXCL_STOP */
#endif /* CONFIG_ZTEST */