Merge "power_supply: Add additional USB PD properties"
This commit is contained in:
commit
cb74f5d749
4 changed files with 300 additions and 107 deletions
|
@ -40,6 +40,9 @@ Optional properties:
|
|||
- vconn-supply: Regulator that enables VCONN source output. This will
|
||||
be supplied on the USB CC line that is not used for
|
||||
communication when Ra resistance is detected.
|
||||
- qcom,vconn-uses-external-source: Indicates whether VCONN supply is sourced
|
||||
from an external regulator. If omitted, then it is
|
||||
assumed it is connected to VBUS.
|
||||
|
||||
Example:
|
||||
qcom,qpnp-pdphy@1700 {
|
||||
|
|
|
@ -267,6 +267,9 @@ static struct device_attribute power_supply_attrs[] = {
|
|||
POWER_SUPPLY_ATTR(typec_power_role),
|
||||
POWER_SUPPLY_ATTR(pd_allowed),
|
||||
POWER_SUPPLY_ATTR(pd_active),
|
||||
POWER_SUPPLY_ATTR(pd_in_hard_reset),
|
||||
POWER_SUPPLY_ATTR(pd_current_max),
|
||||
POWER_SUPPLY_ATTR(pd_usb_suspend_supported),
|
||||
POWER_SUPPLY_ATTR(charger_temp),
|
||||
POWER_SUPPLY_ATTR(charger_temp_max),
|
||||
POWER_SUPPLY_ATTR(parallel_disable),
|
||||
|
|
|
@ -59,6 +59,7 @@ enum usbpd_state {
|
|||
PE_PRS_SRC_SNK_SEND_SWAP,
|
||||
PE_PRS_SRC_SNK_TRANSITION_TO_OFF,
|
||||
PE_PRS_SRC_SNK_WAIT_SOURCE_ON,
|
||||
PE_VCS_WAIT_FOR_VCONN,
|
||||
};
|
||||
|
||||
static const char * const usbpd_state_strings[] = {
|
||||
|
@ -94,6 +95,7 @@ static const char * const usbpd_state_strings[] = {
|
|||
"PRS_SRC_SNK_Send_Swap",
|
||||
"PRS_SRC_SNK_Transition_to_off",
|
||||
"PRS_SRC_SNK_Wait_Source_on",
|
||||
"VCS_Wait_for_VCONN",
|
||||
};
|
||||
|
||||
enum usbpd_control_msg_type {
|
||||
|
@ -170,6 +172,7 @@ static void *usbpd_ipc_log;
|
|||
#define PS_SOURCE_OFF 750
|
||||
#define SWAP_SOURCE_START_TIME 20
|
||||
#define VDM_BUSY_TIME 50
|
||||
#define VCONN_ON_TIME 100
|
||||
|
||||
/* tPSHardReset + tSafe0V + tSrcRecover + tSrcTurnOn */
|
||||
#define SNK_HARD_RESET_RECOVER_TIME (35 + 650 + 1000 + 275)
|
||||
|
@ -195,7 +198,7 @@ static void *usbpd_ipc_log;
|
|||
#define PD_RDO_MISMATCH(rdo) ((rdo) >> 26 & 1)
|
||||
#define PD_RDO_USB_COMM(rdo) ((rdo) >> 25 & 1)
|
||||
#define PD_RDO_NO_USB_SUSP(rdo) ((rdo) >> 24 & 1)
|
||||
#define PD_RDO_FIXED_CURR(rdo) ((rdo) >> 19 & 0x3FF)
|
||||
#define PD_RDO_FIXED_CURR(rdo) ((rdo) >> 10 & 0x3FF)
|
||||
#define PD_RDO_FIXED_CURR_MINMAX(rdo) ((rdo) & 0x3FF)
|
||||
|
||||
#define PD_SRC_PDO_TYPE(pdo) (((pdo) >> 30) & 3)
|
||||
|
@ -223,9 +226,10 @@ static void *usbpd_ipc_log;
|
|||
|
||||
/* VDM header is the first 32-bit object following the 16-bit PD header */
|
||||
#define VDM_HDR_SVID(hdr) ((hdr) >> 16)
|
||||
#define VDM_HDR_TYPE(hdr) ((hdr) & 0x8000)
|
||||
#define VDM_HDR_CMD_TYPE(hdr) (((hdr) >> 6) & 0x3)
|
||||
#define VDM_HDR_CMD(hdr) ((hdr) & 0x1f)
|
||||
#define VDM_IS_SVDM(hdr) ((hdr) & 0x8000)
|
||||
#define SVDM_HDR_OBJ_POS(hdr) (((hdr) >> 8) & 0x7)
|
||||
#define SVDM_HDR_CMD_TYPE(hdr) (((hdr) >> 6) & 0x3)
|
||||
#define SVDM_HDR_CMD(hdr) ((hdr) & 0x1f)
|
||||
|
||||
#define SVDM_HDR(svid, ver, obj, cmd_type, cmd) \
|
||||
(((svid) << 16) | (1 << 15) | ((ver) << 13) \
|
||||
|
@ -253,15 +257,11 @@ static bool ss_dev = true;
|
|||
module_param(ss_dev, bool, S_IRUSR | S_IWUSR);
|
||||
|
||||
static const u32 default_src_caps[] = { 0x36019096 }; /* VSafe5V @ 1.5A */
|
||||
|
||||
static const u32 default_snk_caps[] = { 0x2601905A, /* 5V @ 900mA */
|
||||
0x0002D096, /* 9V @ 1.5A */
|
||||
0x0003C064 }; /* 12V @ 1A */
|
||||
static const u32 default_snk_caps[] = { 0x2601905A }; /* 5V @ 900mA */
|
||||
|
||||
struct vdm_tx {
|
||||
u32 data[7];
|
||||
int size;
|
||||
struct list_head entry;
|
||||
};
|
||||
|
||||
struct usbpd {
|
||||
|
@ -269,6 +269,7 @@ struct usbpd {
|
|||
struct workqueue_struct *wq;
|
||||
struct work_struct sm_work;
|
||||
struct hrtimer timer;
|
||||
bool sm_queued;
|
||||
|
||||
struct extcon_dev *extcon;
|
||||
|
||||
|
@ -307,6 +308,7 @@ struct usbpd {
|
|||
struct regulator *vbus;
|
||||
struct regulator *vconn;
|
||||
bool vconn_enabled;
|
||||
bool vconn_is_external;
|
||||
|
||||
u8 tx_msgid;
|
||||
u8 rx_msgid;
|
||||
|
@ -315,8 +317,9 @@ struct usbpd {
|
|||
|
||||
enum vdm_state vdm_state;
|
||||
u16 *discovered_svids;
|
||||
int num_svids;
|
||||
struct vdm_tx *vdm_tx;
|
||||
struct vdm_tx *vdm_tx_retry;
|
||||
struct list_head vdm_tx_queue;
|
||||
struct list_head svid_handlers;
|
||||
|
||||
struct list_head instance;
|
||||
|
@ -440,6 +443,12 @@ static int pd_select_pdo(struct usbpd *pd, int pdo_pos)
|
|||
}
|
||||
|
||||
pd->requested_voltage = PD_SRC_PDO_FIXED_VOLTAGE(pdo) * 50 * 1000;
|
||||
|
||||
/* Can't sink more than 5V if VCONN is sourced from the VBUS input */
|
||||
if (pd->vconn_enabled && !pd->vconn_is_external &&
|
||||
pd->requested_voltage > 5000000)
|
||||
return -ENOTSUPP;
|
||||
|
||||
pd->requested_current = curr;
|
||||
pd->requested_pdo = pdo_pos;
|
||||
pd->rdo = PD_RDO_FIXED(pdo_pos, 0, mismatch, 1, 1, curr / 10,
|
||||
|
@ -485,6 +494,17 @@ static void pd_send_hard_reset(struct usbpd *pd)
|
|||
pd->hard_reset = true;
|
||||
}
|
||||
|
||||
static void kick_sm(struct usbpd *pd, int ms)
|
||||
{
|
||||
pm_stay_awake(&pd->dev);
|
||||
pd->sm_queued = true;
|
||||
|
||||
if (ms)
|
||||
hrtimer_start(&pd->timer, ms_to_ktime(ms), HRTIMER_MODE_REL);
|
||||
else
|
||||
queue_work(pd->wq, &pd->sm_work);
|
||||
}
|
||||
|
||||
static void phy_sig_received(struct usbpd *pd, enum pd_sig_type type)
|
||||
{
|
||||
if (type != HARD_RESET_SIG) {
|
||||
|
@ -497,7 +517,7 @@ static void phy_sig_received(struct usbpd *pd, enum pd_sig_type type)
|
|||
/* Force CC logic to source/sink to keep Rp/Rd unchanged */
|
||||
set_power_role(pd, pd->current_pr);
|
||||
pd->hard_reset = true;
|
||||
queue_work(pd->wq, &pd->sm_work);
|
||||
kick_sm(pd, 0);
|
||||
}
|
||||
|
||||
static void phy_msg_received(struct usbpd *pd, enum pd_msg_type type,
|
||||
|
@ -534,6 +554,10 @@ static void phy_msg_received(struct usbpd *pd, enum pd_msg_type type,
|
|||
|
||||
pd->rx_msgid = PD_MSG_HDR_ID(header);
|
||||
|
||||
/* discard Pings */
|
||||
if (PD_MSG_HDR_TYPE(header) == MSG_PING && !len)
|
||||
return;
|
||||
|
||||
/* check header's count field to see if it matches len */
|
||||
if (PD_MSG_HDR_COUNT(header) != (len / 4)) {
|
||||
usbpd_err(&pd->dev, "header count (%d) mismatch, len=%ld\n",
|
||||
|
@ -541,11 +565,18 @@ static void phy_msg_received(struct usbpd *pd, enum pd_msg_type type,
|
|||
return;
|
||||
}
|
||||
|
||||
/* block until previous message has been consumed by usbpd_sm */
|
||||
if (pd->rx_msg_type)
|
||||
flush_work(&pd->sm_work);
|
||||
|
||||
pd->rx_msg_type = PD_MSG_HDR_TYPE(header);
|
||||
pd->rx_msg_len = PD_MSG_HDR_COUNT(header);
|
||||
memcpy(&pd->rx_payload, buf, len);
|
||||
|
||||
queue_work(pd->wq, &pd->sm_work);
|
||||
usbpd_dbg(&pd->dev, "received message: type(%d) len(%d)\n",
|
||||
pd->rx_msg_type, pd->rx_msg_len);
|
||||
|
||||
kick_sm(pd, 0);
|
||||
}
|
||||
|
||||
static void phy_shutdown(struct usbpd *pd)
|
||||
|
@ -587,7 +618,7 @@ static void usbpd_set_state(struct usbpd *pd, enum usbpd_state next_state)
|
|||
pd->in_pr_swap = false;
|
||||
set_power_role(pd, PR_NONE);
|
||||
pd->typec_mode = POWER_SUPPLY_TYPEC_NONE;
|
||||
queue_work(pd->wq, &pd->sm_work);
|
||||
kick_sm(pd, 0);
|
||||
break;
|
||||
|
||||
/* Source states */
|
||||
|
@ -634,20 +665,22 @@ static void usbpd_set_state(struct usbpd *pd, enum usbpd_state next_state)
|
|||
pd->current_state = PE_SRC_SEND_CAPABILITIES;
|
||||
if (pd->in_pr_swap) {
|
||||
pd->in_pr_swap = false;
|
||||
hrtimer_start(&pd->timer,
|
||||
ms_to_ktime(SWAP_SOURCE_START_TIME),
|
||||
HRTIMER_MODE_REL);
|
||||
kick_sm(pd, SWAP_SOURCE_START_TIME);
|
||||
break;
|
||||
}
|
||||
|
||||
/* fall-through */
|
||||
|
||||
case PE_SRC_SEND_CAPABILITIES:
|
||||
queue_work(pd->wq, &pd->sm_work);
|
||||
kick_sm(pd, 0);
|
||||
break;
|
||||
|
||||
case PE_SRC_NEGOTIATE_CAPABILITY:
|
||||
if (PD_RDO_OBJ_POS(pd->rdo) != 1) {
|
||||
if (PD_RDO_OBJ_POS(pd->rdo) != 1 ||
|
||||
PD_RDO_FIXED_CURR(pd->rdo) >
|
||||
PD_SRC_PDO_FIXED_MAX_CURR(*default_src_caps) ||
|
||||
PD_RDO_FIXED_CURR_MINMAX(pd->rdo) >
|
||||
PD_SRC_PDO_FIXED_MAX_CURR(*default_src_caps)) {
|
||||
/* send Reject */
|
||||
ret = pd_send_msg(pd, MSG_REJECT, NULL, 0, SOP_MSG);
|
||||
if (ret) {
|
||||
|
@ -717,6 +750,8 @@ static void usbpd_set_state(struct usbpd *pd, enum usbpd_state next_state)
|
|||
break;
|
||||
|
||||
case PE_SRC_TRANSITION_TO_DEFAULT:
|
||||
pd->hard_reset = false;
|
||||
|
||||
if (pd->vconn_enabled)
|
||||
regulator_disable(pd->vconn);
|
||||
regulator_disable(pd->vbus);
|
||||
|
@ -747,7 +782,7 @@ static void usbpd_set_state(struct usbpd *pd, enum usbpd_state next_state)
|
|||
case PE_SRC_HARD_RESET:
|
||||
case PE_SNK_HARD_RESET:
|
||||
/* hard reset may sleep; handle it in the workqueue */
|
||||
queue_work(pd->wq, &pd->sm_work);
|
||||
kick_sm(pd, 0);
|
||||
break;
|
||||
|
||||
case PE_SRC_SEND_SOFT_RESET:
|
||||
|
@ -765,8 +800,7 @@ static void usbpd_set_state(struct usbpd *pd, enum usbpd_state next_state)
|
|||
}
|
||||
|
||||
/* wait for ACCEPT */
|
||||
hrtimer_start(&pd->timer, ms_to_ktime(SENDER_RESPONSE_TIME),
|
||||
HRTIMER_MODE_REL);
|
||||
kick_sm(pd, SENDER_RESPONSE_TIME);
|
||||
break;
|
||||
|
||||
/* Sink states */
|
||||
|
@ -786,9 +820,11 @@ static void usbpd_set_state(struct usbpd *pd, enum usbpd_state next_state)
|
|||
}
|
||||
}
|
||||
|
||||
/* Reset protocol layer */
|
||||
pd->tx_msgid = 0;
|
||||
pd->rx_msgid = -1;
|
||||
pd->rx_msg_len = 0;
|
||||
pd->rx_msg_type = 0;
|
||||
pd->rx_msgid = -1;
|
||||
|
||||
if (!pd->in_pr_swap) {
|
||||
if (pd->pd_phy_opened) {
|
||||
|
@ -819,11 +855,9 @@ static void usbpd_set_state(struct usbpd *pd, enum usbpd_state next_state)
|
|||
|
||||
case PE_SNK_WAIT_FOR_CAPABILITIES:
|
||||
if (pd->rx_msg_len && pd->rx_msg_type)
|
||||
queue_work(pd->wq, &pd->sm_work);
|
||||
kick_sm(pd, 0);
|
||||
else
|
||||
hrtimer_start(&pd->timer,
|
||||
ms_to_ktime(SINK_WAIT_CAP_TIME),
|
||||
HRTIMER_MODE_REL);
|
||||
kick_sm(pd, SINK_WAIT_CAP_TIME);
|
||||
break;
|
||||
|
||||
case PE_SNK_EVALUATE_CAPABILITY:
|
||||
|
@ -841,18 +875,19 @@ static void usbpd_set_state(struct usbpd *pd, enum usbpd_state next_state)
|
|||
|
||||
case PE_SNK_SELECT_CAPABILITY:
|
||||
ret = pd_send_msg(pd, MSG_REQUEST, &pd->rdo, 1, SOP_MSG);
|
||||
if (ret)
|
||||
if (ret) {
|
||||
usbpd_err(&pd->dev, "Error sending Request\n");
|
||||
usbpd_set_state(pd, PE_SNK_SEND_SOFT_RESET);
|
||||
break;
|
||||
}
|
||||
|
||||
/* wait for ACCEPT */
|
||||
hrtimer_start(&pd->timer, ms_to_ktime(SENDER_RESPONSE_TIME),
|
||||
HRTIMER_MODE_REL);
|
||||
kick_sm(pd, SENDER_RESPONSE_TIME);
|
||||
break;
|
||||
|
||||
case PE_SNK_TRANSITION_SINK:
|
||||
/* wait for PS_RDY */
|
||||
hrtimer_start(&pd->timer, ms_to_ktime(PS_TRANSITION_TIME),
|
||||
HRTIMER_MODE_REL);
|
||||
kick_sm(pd, PS_TRANSITION_TIME);
|
||||
break;
|
||||
|
||||
case PE_SNK_READY:
|
||||
|
@ -861,6 +896,8 @@ static void usbpd_set_state(struct usbpd *pd, enum usbpd_state next_state)
|
|||
break;
|
||||
|
||||
case PE_SNK_TRANSITION_TO_DEFAULT:
|
||||
pd->hard_reset = false;
|
||||
|
||||
if (pd->current_dr != DR_UFP) {
|
||||
extcon_set_cable_state_(pd->extcon, EXTCON_USB_HOST, 0);
|
||||
|
||||
|
@ -877,17 +914,13 @@ static void usbpd_set_state(struct usbpd *pd, enum usbpd_state next_state)
|
|||
pd->vconn_enabled = false;
|
||||
}
|
||||
|
||||
pd->tx_msgid = 0;
|
||||
|
||||
val.intval = pd->requested_voltage; /* set range back to 5V */
|
||||
power_supply_set_property(pd->usb_psy,
|
||||
POWER_SUPPLY_PROP_VOLTAGE_MAX, &val);
|
||||
pd->current_voltage = pd->requested_voltage;
|
||||
|
||||
/* max time for hard reset to toggle vbus off/on */
|
||||
hrtimer_start(&pd->timer,
|
||||
ms_to_ktime(SNK_HARD_RESET_RECOVER_TIME),
|
||||
HRTIMER_MODE_REL);
|
||||
kick_sm(pd, SNK_HARD_RESET_RECOVER_TIME);
|
||||
break;
|
||||
|
||||
case PE_PRS_SNK_SRC_TRANSITION_TO_OFF:
|
||||
|
@ -904,8 +937,7 @@ static void usbpd_set_state(struct usbpd *pd, enum usbpd_state next_state)
|
|||
pd_phy_update_roles(pd->current_dr, PR_SRC);
|
||||
|
||||
/* wait for PS_RDY */
|
||||
hrtimer_start(&pd->timer, ms_to_ktime(PS_SOURCE_OFF),
|
||||
HRTIMER_MODE_REL);
|
||||
kick_sm(pd, PS_SOURCE_OFF);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -929,10 +961,10 @@ int usbpd_register_svid(struct usbpd *pd, struct usbpd_svid_handler *hdlr)
|
|||
|
||||
/* already connected with this SVID discovered? */
|
||||
if (pd->vdm_state >= DISCOVERED_SVIDS) {
|
||||
u16 *psvid;
|
||||
int i;
|
||||
|
||||
for (psvid = pd->discovered_svids; *psvid; psvid++) {
|
||||
if (*psvid == hdlr->svid) {
|
||||
for (i = 0; i < pd->num_svids; i++) {
|
||||
if (pd->discovered_svids[i] == hdlr->svid) {
|
||||
if (hdlr->connect)
|
||||
hdlr->connect(hdlr);
|
||||
break;
|
||||
|
@ -954,7 +986,7 @@ int usbpd_send_vdm(struct usbpd *pd, u32 vdm_hdr, const u32 *vdos, int num_vdos)
|
|||
{
|
||||
struct vdm_tx *vdm_tx;
|
||||
|
||||
if (!pd->in_explicit_contract)
|
||||
if (!pd->in_explicit_contract || pd->vdm_tx)
|
||||
return -EBUSY;
|
||||
|
||||
vdm_tx = kzalloc(sizeof(*vdm_tx), GFP_KERNEL);
|
||||
|
@ -967,8 +999,10 @@ int usbpd_send_vdm(struct usbpd *pd, u32 vdm_hdr, const u32 *vdos, int num_vdos)
|
|||
vdm_tx->size = num_vdos + 1; /* include the header */
|
||||
|
||||
/* VDM will get sent in PE_SRC/SNK_READY state handling */
|
||||
list_add_tail(&vdm_tx->entry, &pd->vdm_tx_queue);
|
||||
queue_work(pd->wq, &pd->sm_work);
|
||||
pd->vdm_tx = vdm_tx;
|
||||
|
||||
/* slight delay before queuing to prioritize handling of incoming VDM */
|
||||
kick_sm(pd, 5);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
@ -994,8 +1028,8 @@ static void handle_vdm_rx(struct usbpd *pd)
|
|||
u16 svid = VDM_HDR_SVID(vdm_hdr);
|
||||
u16 *psvid;
|
||||
u8 i, num_vdos = pd->rx_msg_len - 1; /* num objects minus header */
|
||||
u8 cmd = VDM_HDR_CMD(vdm_hdr);
|
||||
u8 cmd_type = VDM_HDR_CMD_TYPE(vdm_hdr);
|
||||
u8 cmd = SVDM_HDR_CMD(vdm_hdr);
|
||||
u8 cmd_type = SVDM_HDR_CMD_TYPE(vdm_hdr);
|
||||
struct usbpd_svid_handler *handler;
|
||||
|
||||
usbpd_dbg(&pd->dev, "VDM rx: svid:%x cmd:%x cmd_type:%x vdm_hdr:%x\n",
|
||||
|
@ -1005,7 +1039,7 @@ static void handle_vdm_rx(struct usbpd *pd)
|
|||
handler = find_svid_handler(pd, svid);
|
||||
|
||||
/* Unstructured VDM */
|
||||
if (!VDM_HDR_TYPE(vdm_hdr)) {
|
||||
if (!VDM_IS_SVDM(vdm_hdr)) {
|
||||
if (handler && handler->vdm_received)
|
||||
handler->vdm_received(handler, vdm_hdr, vdos, num_vdos);
|
||||
return;
|
||||
|
@ -1016,7 +1050,19 @@ static void handle_vdm_rx(struct usbpd *pd)
|
|||
|
||||
switch (cmd_type) {
|
||||
case SVDM_CMD_TYPE_INITIATOR:
|
||||
if (cmd == USBPD_SVDM_DISCOVER_IDENTITY) {
|
||||
/*
|
||||
* if this interrupts a previous exchange, abort the previous
|
||||
* outgoing response
|
||||
*/
|
||||
if (pd->vdm_tx) {
|
||||
usbpd_dbg(&pd->dev, "Discarding previously queued SVDM tx (SVID:0x%04x)\n",
|
||||
VDM_HDR_SVID(pd->vdm_tx->data[0]));
|
||||
|
||||
kfree(pd->vdm_tx);
|
||||
pd->vdm_tx = NULL;
|
||||
}
|
||||
|
||||
if (svid == USBPD_SID && cmd == USBPD_SVDM_DISCOVER_IDENTITY) {
|
||||
u32 tx_vdos[3] = {
|
||||
ID_HDR_USB_HOST | ID_HDR_USB_DEVICE |
|
||||
ID_HDR_PRODUCT_PER_MASK | ID_HDR_VID,
|
||||
|
@ -1027,9 +1073,9 @@ static void handle_vdm_rx(struct usbpd *pd)
|
|||
|
||||
usbpd_send_svdm(pd, USBPD_SID, cmd,
|
||||
SVDM_CMD_TYPE_RESP_ACK, 0, tx_vdos, 3);
|
||||
} else {
|
||||
usbpd_send_svdm(pd, USBPD_SID, cmd,
|
||||
SVDM_CMD_TYPE_RESP_NAK, 0, NULL, 0);
|
||||
} else if (cmd != USBPD_SVDM_ATTENTION) {
|
||||
usbpd_send_svdm(pd, svid, cmd, SVDM_CMD_TYPE_RESP_NAK,
|
||||
SVDM_HDR_OBJ_POS(vdm_hdr), NULL, 0);
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -1061,10 +1107,9 @@ static void handle_vdm_rx(struct usbpd *pd)
|
|||
kfree(pd->vdm_tx_retry);
|
||||
pd->vdm_tx_retry = NULL;
|
||||
|
||||
kfree(pd->discovered_svids);
|
||||
|
||||
/* TODO: handle > 12 SVIDs */
|
||||
pd->discovered_svids = kzalloc((2 * num_vdos + 1) *
|
||||
if (!pd->discovered_svids) {
|
||||
pd->num_svids = 2 * num_vdos;
|
||||
pd->discovered_svids = kcalloc(pd->num_svids,
|
||||
sizeof(u16),
|
||||
GFP_KERNEL);
|
||||
if (!pd->discovered_svids) {
|
||||
|
@ -1072,8 +1117,27 @@ static void handle_vdm_rx(struct usbpd *pd)
|
|||
break;
|
||||
}
|
||||
|
||||
/* convert 32-bit VDOs to list of 16-bit SVIDs */
|
||||
psvid = pd->discovered_svids;
|
||||
} else { /* handle > 12 SVIDs */
|
||||
void *ptr;
|
||||
size_t oldsize = pd->num_svids * sizeof(u16);
|
||||
size_t newsize = oldsize +
|
||||
(2 * num_vdos * sizeof(u16));
|
||||
|
||||
ptr = krealloc(pd->discovered_svids, newsize,
|
||||
GFP_KERNEL);
|
||||
if (!ptr) {
|
||||
usbpd_err(&pd->dev, "unable to realloc SVIDs\n");
|
||||
break;
|
||||
}
|
||||
|
||||
pd->discovered_svids = ptr;
|
||||
psvid = pd->discovered_svids + pd->num_svids;
|
||||
memset(psvid, 0, (2 * num_vdos));
|
||||
pd->num_svids += 2 * num_vdos;
|
||||
}
|
||||
|
||||
/* convert 32-bit VDOs to list of 16-bit SVIDs */
|
||||
for (i = 0; i < num_vdos * 2; i++) {
|
||||
/*
|
||||
* Within each 32-bit VDO,
|
||||
|
@ -1097,8 +1161,22 @@ static void handle_vdm_rx(struct usbpd *pd)
|
|||
usbpd_dbg(&pd->dev, "Discovered SVID: 0x%04x\n",
|
||||
svid);
|
||||
*psvid++ = svid;
|
||||
}
|
||||
}
|
||||
|
||||
/* if SVID supported notify handler */
|
||||
/* if more than 12 SVIDs, resend the request */
|
||||
if (num_vdos == 6 && vdos[5] != 0) {
|
||||
usbpd_send_svdm(pd, USBPD_SID,
|
||||
USBPD_SVDM_DISCOVER_SVIDS,
|
||||
SVDM_CMD_TYPE_INITIATOR, 0,
|
||||
NULL, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
/* now that all SVIDs are discovered, notify handlers */
|
||||
for (i = 0; i < pd->num_svids; i++) {
|
||||
svid = pd->discovered_svids[i];
|
||||
if (svid) {
|
||||
handler = find_svid_handler(pd, svid);
|
||||
if (handler && handler->connect)
|
||||
handler->connect(handler);
|
||||
|
@ -1148,10 +1226,9 @@ static void handle_vdm_rx(struct usbpd *pd)
|
|||
}
|
||||
|
||||
/* wait tVDMBusy, then retry */
|
||||
list_move(&pd->vdm_tx_retry->entry, &pd->vdm_tx_queue);
|
||||
pd->vdm_tx = pd->vdm_tx_retry;
|
||||
pd->vdm_tx_retry = NULL;
|
||||
hrtimer_start(&pd->timer, ms_to_ktime(VDM_BUSY_TIME),
|
||||
HRTIMER_MODE_REL);
|
||||
kick_sm(pd, VDM_BUSY_TIME);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -1165,16 +1242,14 @@ static void handle_vdm_tx(struct usbpd *pd)
|
|||
int ret;
|
||||
|
||||
/* only send one VDM at a time */
|
||||
if (!list_empty(&pd->vdm_tx_queue)) {
|
||||
struct vdm_tx *vdm_tx = list_first_entry(&pd->vdm_tx_queue,
|
||||
struct vdm_tx, entry);
|
||||
u32 vdm_hdr = vdm_tx->data[0];
|
||||
if (pd->vdm_tx) {
|
||||
u32 vdm_hdr = pd->vdm_tx->data[0];
|
||||
|
||||
ret = pd_send_msg(pd, MSG_VDM, vdm_tx->data, vdm_tx->size,
|
||||
SOP_MSG);
|
||||
ret = pd_send_msg(pd, MSG_VDM, pd->vdm_tx->data,
|
||||
pd->vdm_tx->size, SOP_MSG);
|
||||
if (ret) {
|
||||
usbpd_err(&pd->dev, "Error sending VDM command %d\n",
|
||||
VDM_HDR_CMD(vdm_tx->data[0]));
|
||||
SVDM_HDR_CMD(pd->vdm_tx->data[0]));
|
||||
usbpd_set_state(pd, pd->current_pr == PR_SRC ?
|
||||
PE_SRC_SEND_SOFT_RESET :
|
||||
PE_SNK_SEND_SOFT_RESET);
|
||||
|
@ -1183,24 +1258,25 @@ static void handle_vdm_tx(struct usbpd *pd)
|
|||
return;
|
||||
}
|
||||
|
||||
list_del(&vdm_tx->entry);
|
||||
|
||||
/*
|
||||
* special case: keep initiated Discover ID/SVIDs
|
||||
* around in case we need to re-try when receiving BUSY
|
||||
*/
|
||||
if (VDM_HDR_TYPE(vdm_hdr) &&
|
||||
VDM_HDR_CMD_TYPE(vdm_hdr) == SVDM_CMD_TYPE_INITIATOR &&
|
||||
VDM_HDR_CMD(vdm_hdr) <= USBPD_SVDM_DISCOVER_SVIDS) {
|
||||
if (VDM_IS_SVDM(vdm_hdr) &&
|
||||
SVDM_HDR_CMD_TYPE(vdm_hdr) == SVDM_CMD_TYPE_INITIATOR &&
|
||||
SVDM_HDR_CMD(vdm_hdr) <= USBPD_SVDM_DISCOVER_SVIDS) {
|
||||
if (pd->vdm_tx_retry) {
|
||||
usbpd_err(&pd->dev, "Previous Discover VDM command %d not ACKed/NAKed\n",
|
||||
VDM_HDR_CMD(pd->vdm_tx_retry->data[0]));
|
||||
SVDM_HDR_CMD(
|
||||
pd->vdm_tx_retry->data[0]));
|
||||
kfree(pd->vdm_tx_retry);
|
||||
}
|
||||
pd->vdm_tx_retry = vdm_tx;
|
||||
pd->vdm_tx_retry = pd->vdm_tx;
|
||||
} else {
|
||||
kfree(vdm_tx);
|
||||
kfree(pd->vdm_tx);
|
||||
}
|
||||
|
||||
pd->vdm_tx = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1216,13 +1292,9 @@ static void reset_vdm_state(struct usbpd *pd)
|
|||
pd->vdm_tx_retry = NULL;
|
||||
kfree(pd->discovered_svids);
|
||||
pd->discovered_svids = NULL;
|
||||
while (!list_empty(&pd->vdm_tx_queue)) {
|
||||
struct vdm_tx *vdm_tx =
|
||||
list_first_entry(&pd->vdm_tx_queue,
|
||||
struct vdm_tx, entry);
|
||||
list_del(&vdm_tx->entry);
|
||||
kfree(vdm_tx);
|
||||
}
|
||||
pd->num_svids = 0;
|
||||
kfree(pd->vdm_tx);
|
||||
pd->vdm_tx = NULL;
|
||||
}
|
||||
|
||||
static void dr_swap(struct usbpd *pd)
|
||||
|
@ -1252,6 +1324,34 @@ static void dr_swap(struct usbpd *pd)
|
|||
pd_phy_update_roles(pd->current_dr, pd->current_pr);
|
||||
}
|
||||
|
||||
|
||||
static void vconn_swap(struct usbpd *pd)
|
||||
{
|
||||
int ret;
|
||||
|
||||
if (pd->vconn_enabled) {
|
||||
pd->current_state = PE_VCS_WAIT_FOR_VCONN;
|
||||
kick_sm(pd, VCONN_ON_TIME);
|
||||
} else {
|
||||
ret = regulator_enable(pd->vconn);
|
||||
if (ret) {
|
||||
usbpd_err(&pd->dev, "Unable to enable vconn\n");
|
||||
return;
|
||||
}
|
||||
|
||||
pd->vconn_enabled = true;
|
||||
|
||||
ret = pd_send_msg(pd, MSG_PS_RDY, NULL, 0, SOP_MSG);
|
||||
if (ret) {
|
||||
usbpd_err(&pd->dev, "Error sending PS_RDY\n");
|
||||
usbpd_set_state(pd, pd->current_pr == PR_SRC ?
|
||||
PE_SRC_SEND_SOFT_RESET :
|
||||
PE_SNK_SEND_SOFT_RESET);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Handles current state and determines transitions */
|
||||
static void usbpd_sm(struct work_struct *w)
|
||||
{
|
||||
|
@ -1265,6 +1365,7 @@ static void usbpd_sm(struct work_struct *w)
|
|||
usbpd_state_strings[pd->current_state]);
|
||||
|
||||
hrtimer_cancel(&pd->timer);
|
||||
pd->sm_queued = false;
|
||||
|
||||
if (pd->rx_msg_len)
|
||||
data_recvd = pd->rx_msg_type;
|
||||
|
@ -1274,7 +1375,7 @@ static void usbpd_sm(struct work_struct *w)
|
|||
/* Disconnect? */
|
||||
if (pd->typec_mode == POWER_SUPPLY_TYPEC_NONE) {
|
||||
if (pd->current_state == PE_UNKNOWN)
|
||||
return;
|
||||
goto sm_done;
|
||||
|
||||
usbpd_info(&pd->dev, "USB PD disconnect\n");
|
||||
|
||||
|
@ -1328,7 +1429,7 @@ static void usbpd_sm(struct work_struct *w)
|
|||
|
||||
pd->current_state = PE_UNKNOWN;
|
||||
|
||||
return;
|
||||
goto sm_done;
|
||||
}
|
||||
|
||||
/* Hard reset? */
|
||||
|
@ -1339,7 +1440,8 @@ static void usbpd_sm(struct work_struct *w)
|
|||
usbpd_set_state(pd, PE_SNK_TRANSITION_TO_DEFAULT);
|
||||
else
|
||||
usbpd_set_state(pd, PE_SRC_TRANSITION_TO_DEFAULT);
|
||||
pd->hard_reset = false;
|
||||
|
||||
goto sm_done;
|
||||
}
|
||||
|
||||
/* Soft reset? */
|
||||
|
@ -1400,8 +1502,7 @@ static void usbpd_sm(struct work_struct *w)
|
|||
break;
|
||||
}
|
||||
|
||||
hrtimer_start(&pd->timer, ms_to_ktime(SRC_CAP_TIME),
|
||||
HRTIMER_MODE_REL);
|
||||
kick_sm(pd, SRC_CAP_TIME);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -1416,14 +1517,16 @@ static void usbpd_sm(struct work_struct *w)
|
|||
|
||||
/* wait for REQUEST */
|
||||
pd->current_state = PE_SRC_SEND_CAPABILITIES_WAIT;
|
||||
hrtimer_start(&pd->timer, ms_to_ktime(SENDER_RESPONSE_TIME),
|
||||
HRTIMER_MODE_REL);
|
||||
kick_sm(pd, SENDER_RESPONSE_TIME);
|
||||
break;
|
||||
|
||||
case PE_SRC_SEND_CAPABILITIES_WAIT:
|
||||
if (data_recvd == MSG_REQUEST) {
|
||||
pd->rdo = pd->rx_payload[0];
|
||||
usbpd_set_state(pd, PE_SRC_NEGOTIATE_CAPABILITY);
|
||||
} else if (data_recvd || ctrl_recvd) {
|
||||
usbpd_err(&pd->dev, "Unexpected message received\n");
|
||||
usbpd_set_state(pd, PE_SRC_SEND_SOFT_RESET);
|
||||
} else {
|
||||
usbpd_set_state(pd, PE_SRC_HARD_RESET);
|
||||
}
|
||||
|
@ -1439,6 +1542,14 @@ static void usbpd_sm(struct work_struct *w)
|
|||
usbpd_set_state(pd, PE_SRC_SEND_SOFT_RESET);
|
||||
break;
|
||||
}
|
||||
} else if (ctrl_recvd == MSG_GET_SINK_CAP) {
|
||||
ret = pd_send_msg(pd, MSG_SINK_CAPABILITIES,
|
||||
default_snk_caps,
|
||||
ARRAY_SIZE(default_snk_caps), SOP_MSG);
|
||||
if (ret) {
|
||||
usbpd_err(&pd->dev, "Error sending Sink Caps\n");
|
||||
usbpd_set_state(pd, PE_SRC_SEND_SOFT_RESET);
|
||||
}
|
||||
} else if (data_recvd == MSG_REQUEST) {
|
||||
pd->rdo = pd->rx_payload[0];
|
||||
usbpd_set_state(pd, PE_SRC_NEGOTIATE_CAPABILITY);
|
||||
|
@ -1470,10 +1581,17 @@ static void usbpd_sm(struct work_struct *w)
|
|||
}
|
||||
|
||||
pd->current_state = PE_PRS_SRC_SNK_TRANSITION_TO_OFF;
|
||||
hrtimer_start(&pd->timer,
|
||||
ms_to_ktime(SRC_TRANSITION_TIME),
|
||||
HRTIMER_MODE_REL);
|
||||
kick_sm(pd, SRC_TRANSITION_TIME);
|
||||
break;
|
||||
} else if (ctrl_recvd == MSG_VCONN_SWAP) {
|
||||
ret = pd_send_msg(pd, MSG_ACCEPT, NULL, 0, SOP_MSG);
|
||||
if (ret) {
|
||||
usbpd_err(&pd->dev, "Error sending Accept\n");
|
||||
usbpd_set_state(pd, PE_SRC_SEND_SOFT_RESET);
|
||||
break;
|
||||
}
|
||||
|
||||
vconn_swap(pd);
|
||||
} else {
|
||||
if (data_recvd == MSG_VDM)
|
||||
handle_vdm_rx(pd);
|
||||
|
@ -1538,6 +1656,9 @@ static void usbpd_sm(struct work_struct *w)
|
|||
else
|
||||
usbpd_set_state(pd,
|
||||
PE_SNK_WAIT_FOR_CAPABILITIES);
|
||||
} else if (pd->rx_msg_type) {
|
||||
usbpd_err(&pd->dev, "Invalid response to sink request\n");
|
||||
usbpd_set_state(pd, PE_SNK_SEND_SOFT_RESET);
|
||||
} else {
|
||||
/* timed out; go to hard reset */
|
||||
usbpd_set_state(pd, PE_SNK_HARD_RESET);
|
||||
|
@ -1566,9 +1687,9 @@ static void usbpd_sm(struct work_struct *w)
|
|||
break;
|
||||
|
||||
case PE_SNK_READY:
|
||||
if (data_recvd == MSG_SOURCE_CAPABILITIES)
|
||||
if (data_recvd == MSG_SOURCE_CAPABILITIES) {
|
||||
usbpd_set_state(pd, PE_SNK_EVALUATE_CAPABILITY);
|
||||
else if (ctrl_recvd == MSG_GET_SINK_CAP) {
|
||||
} else if (ctrl_recvd == MSG_GET_SINK_CAP) {
|
||||
ret = pd_send_msg(pd, MSG_SINK_CAPABILITIES,
|
||||
default_snk_caps,
|
||||
ARRAY_SIZE(default_snk_caps), SOP_MSG);
|
||||
|
@ -1576,6 +1697,15 @@ static void usbpd_sm(struct work_struct *w)
|
|||
usbpd_err(&pd->dev, "Error sending Sink Caps\n");
|
||||
usbpd_set_state(pd, PE_SNK_SEND_SOFT_RESET);
|
||||
}
|
||||
} else if (ctrl_recvd == MSG_GET_SOURCE_CAP) {
|
||||
ret = pd_send_msg(pd, MSG_SOURCE_CAPABILITIES,
|
||||
default_src_caps,
|
||||
ARRAY_SIZE(default_src_caps), SOP_MSG);
|
||||
if (ret) {
|
||||
usbpd_err(&pd->dev, "Error sending SRC CAPs\n");
|
||||
usbpd_set_state(pd, PE_SNK_SEND_SOFT_RESET);
|
||||
break;
|
||||
}
|
||||
} else if (ctrl_recvd == MSG_DR_SWAP) {
|
||||
if (pd->vdm_state == MODE_ENTERED) {
|
||||
usbpd_set_state(pd, PE_SNK_HARD_RESET);
|
||||
|
@ -1606,6 +1736,32 @@ static void usbpd_sm(struct work_struct *w)
|
|||
pd->in_pr_swap = true;
|
||||
usbpd_set_state(pd, PE_PRS_SNK_SRC_TRANSITION_TO_OFF);
|
||||
break;
|
||||
} else if (ctrl_recvd == MSG_VCONN_SWAP) {
|
||||
/*
|
||||
* if VCONN is connected to VBUS, make sure we are
|
||||
* not in high voltage contract, otherwise reject.
|
||||
*/
|
||||
if (!pd->vconn_is_external &&
|
||||
(pd->requested_voltage > 5000000)) {
|
||||
ret = pd_send_msg(pd, MSG_REJECT, NULL, 0,
|
||||
SOP_MSG);
|
||||
if (ret) {
|
||||
usbpd_err(&pd->dev, "Error sending Reject\n");
|
||||
usbpd_set_state(pd,
|
||||
PE_SNK_SEND_SOFT_RESET);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
ret = pd_send_msg(pd, MSG_ACCEPT, NULL, 0, SOP_MSG);
|
||||
if (ret) {
|
||||
usbpd_err(&pd->dev, "Error sending Accept\n");
|
||||
usbpd_set_state(pd, PE_SNK_SEND_SOFT_RESET);
|
||||
break;
|
||||
}
|
||||
|
||||
vconn_swap(pd);
|
||||
} else {
|
||||
if (data_recvd == MSG_VDM)
|
||||
handle_vdm_rx(pd);
|
||||
|
@ -1623,7 +1779,7 @@ static void usbpd_sm(struct work_struct *w)
|
|||
POWER_SUPPLY_PROP_TYPEC_MODE, &val);
|
||||
if (val.intval == POWER_SUPPLY_TYPEC_NONE) {
|
||||
pd->typec_mode = POWER_SUPPLY_TYPEC_NONE;
|
||||
queue_work(pd->wq, &pd->sm_work);
|
||||
kick_sm(pd, 0);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -1701,8 +1857,7 @@ static void usbpd_sm(struct work_struct *w)
|
|||
}
|
||||
|
||||
pd->current_state = PE_PRS_SRC_SNK_TRANSITION_TO_OFF;
|
||||
hrtimer_start(&pd->timer, ms_to_ktime(SRC_TRANSITION_TIME),
|
||||
HRTIMER_MODE_REL);
|
||||
kick_sm(pd, SRC_TRANSITION_TIME);
|
||||
break;
|
||||
|
||||
case PE_PRS_SRC_SNK_TRANSITION_TO_OFF:
|
||||
|
@ -1727,8 +1882,7 @@ static void usbpd_sm(struct work_struct *w)
|
|||
}
|
||||
|
||||
pd->current_state = PE_PRS_SRC_SNK_WAIT_SOURCE_ON;
|
||||
hrtimer_start(&pd->timer, ms_to_ktime(PS_SOURCE_ON),
|
||||
HRTIMER_MODE_REL);
|
||||
kick_sm(pd, PS_SOURCE_ON);
|
||||
break;
|
||||
|
||||
case PE_PRS_SRC_SNK_WAIT_SOURCE_ON:
|
||||
|
@ -1778,6 +1932,26 @@ static void usbpd_sm(struct work_struct *w)
|
|||
usbpd_set_state(pd, PE_SRC_STARTUP);
|
||||
break;
|
||||
|
||||
case PE_VCS_WAIT_FOR_VCONN:
|
||||
if (ctrl_recvd == MSG_PS_RDY) {
|
||||
/*
|
||||
* hopefully redundant check but in case not enabled
|
||||
* avoids unbalanced regulator disable count
|
||||
*/
|
||||
if (pd->vconn_enabled)
|
||||
regulator_disable(pd->vconn);
|
||||
pd->vconn_enabled = false;
|
||||
|
||||
pd->current_state = pd->current_pr == PR_SRC ?
|
||||
PE_SRC_READY : PE_SNK_READY;
|
||||
} else {
|
||||
/* timed out; go to hard reset */
|
||||
usbpd_set_state(pd, pd->current_pr == PR_SRC ?
|
||||
PE_SRC_HARD_RESET : PE_SNK_HARD_RESET);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
usbpd_err(&pd->dev, "Unhandled state %s\n",
|
||||
usbpd_state_strings[pd->current_state]);
|
||||
|
@ -1786,6 +1960,10 @@ static void usbpd_sm(struct work_struct *w)
|
|||
|
||||
/* Rx message should have been consumed now */
|
||||
pd->rx_msg_type = pd->rx_msg_len = 0;
|
||||
|
||||
sm_done:
|
||||
if (!pd->sm_queued)
|
||||
pm_relax(&pd->dev);
|
||||
}
|
||||
|
||||
static inline const char *src_current(enum power_supply_typec_mode typec_mode)
|
||||
|
@ -1897,7 +2075,7 @@ static int psy_changed(struct notifier_block *nb, unsigned long evt, void *ptr)
|
|||
switch (typec_mode) {
|
||||
/* Disconnect */
|
||||
case POWER_SUPPLY_TYPEC_NONE:
|
||||
queue_work(pd->wq, &pd->sm_work);
|
||||
kick_sm(pd, 0);
|
||||
break;
|
||||
|
||||
/* Sink states */
|
||||
|
@ -1909,7 +2087,7 @@ static int psy_changed(struct notifier_block *nb, unsigned long evt, void *ptr)
|
|||
if (pd->current_pr != PR_SINK ||
|
||||
pd->current_state == PE_SNK_TRANSITION_TO_DEFAULT) {
|
||||
pd->current_pr = PR_SINK;
|
||||
queue_work(pd->wq, &pd->sm_work);
|
||||
kick_sm(pd, 0);
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -1921,7 +2099,7 @@ static int psy_changed(struct notifier_block *nb, unsigned long evt, void *ptr)
|
|||
"" : " (powered)");
|
||||
if (pd->current_pr != PR_SRC) {
|
||||
pd->current_pr = PR_SRC;
|
||||
queue_work(pd->wq, &pd->sm_work);
|
||||
kick_sm(pd, 0);
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -2359,6 +2537,10 @@ struct usbpd *usbpd_create(struct device *parent)
|
|||
if (ret)
|
||||
goto free_pd;
|
||||
|
||||
ret = device_init_wakeup(&pd->dev, true);
|
||||
if (ret)
|
||||
goto free_pd;
|
||||
|
||||
ret = device_add(&pd->dev);
|
||||
if (ret)
|
||||
goto free_pd;
|
||||
|
@ -2414,11 +2596,13 @@ struct usbpd *usbpd_create(struct device *parent)
|
|||
goto unreg_psy;
|
||||
}
|
||||
|
||||
pd->vconn_is_external = device_property_present(parent,
|
||||
"qcom,vconn-uses-external-source");
|
||||
|
||||
pd->current_pr = PR_NONE;
|
||||
pd->current_dr = DR_NONE;
|
||||
list_add_tail(&pd->instance, &_usbpd);
|
||||
|
||||
INIT_LIST_HEAD(&pd->vdm_tx_queue);
|
||||
INIT_LIST_HEAD(&pd->svid_handlers);
|
||||
|
||||
/* force read initial power_supply values */
|
||||
|
|
|
@ -216,6 +216,9 @@ enum power_supply_property {
|
|||
POWER_SUPPLY_PROP_TYPEC_POWER_ROLE,
|
||||
POWER_SUPPLY_PROP_PD_ALLOWED,
|
||||
POWER_SUPPLY_PROP_PD_ACTIVE,
|
||||
POWER_SUPPLY_PROP_PD_IN_HARD_RESET,
|
||||
POWER_SUPPLY_PROP_PD_CURRENT_MAX,
|
||||
POWER_SUPPLY_PROP_PD_USB_SUSPEND_SUPPORTED,
|
||||
POWER_SUPPLY_PROP_CHARGER_TEMP,
|
||||
POWER_SUPPLY_PROP_CHARGER_TEMP_MAX,
|
||||
POWER_SUPPLY_PROP_PARALLEL_DISABLE,
|
||||
|
|
Loading…
Add table
Reference in a new issue