From patchwork Wed Feb 1 04:18:50 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Bjorn Andersson X-Patchwork-Id: 650868 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 9081CC63797 for ; Wed, 1 Feb 2023 04:19:08 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S231362AbjBAETH (ORCPT ); Tue, 31 Jan 2023 23:19:07 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:34996 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S231285AbjBAETF (ORCPT ); Tue, 31 Jan 2023 23:19:05 -0500 Received: from mx0b-0031df01.pphosted.com (mx0b-0031df01.pphosted.com [205.220.180.131]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id C75B9402F7; Tue, 31 Jan 2023 20:19:04 -0800 (PST) Received: from pps.filterd (m0279870.ppops.net [127.0.0.1]) by mx0a-0031df01.pphosted.com (8.17.1.19/8.17.1.19) with ESMTP id 3113PDQk031113; Wed, 1 Feb 2023 04:19:00 GMT DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=quicinc.com; h=from : to : cc : subject : date : message-id : in-reply-to : references : mime-version : content-transfer-encoding : content-type; s=qcppdkim1; bh=PQt3fQopKUNxXskBOhMqwr8e4wI+me9M7xC10H3pHa8=; b=Xy6+L65DY0FpIu52lPvlqijj5zL1B7SL8St/TDzohXOHTHUilMkLGect6zddwN+NZPEU 5q4kuUuQrBGLxhBzJ/wmsZgdwqH5OJvAhTJ2eWUSve3q7ZFcDJLatq9meyWc/BxxfPIh FXT2yxjLOBXD/XWy0y5y9fnRehCRLS6XKrI/557RGg864CECceDZ1/b6kA3tBbxjyo1s +NsfRt5OuBvH2+IEx7fI/yl4AgsPTQiN4z5Qus3DM6/Va3oqB5z/MIGV4FGlRXP1iGYR VH5neS8ub0iSdTXEk9q3yT1WBLm8/Lj2r0chRf7Erf/10MS64+Qv5UJqCj/dvQURRSJq RQ== Received: from nalasppmta02.qualcomm.com (Global_NAT1.qualcomm.com [129.46.96.20]) by mx0a-0031df01.pphosted.com (PPS) with ESMTPS id 3neua9b4p2-1 (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NOT); Wed, 01 Feb 2023 04:19:00 +0000 Received: from nalasex01c.na.qualcomm.com (nalasex01c.na.qualcomm.com [10.47.97.35]) by NALASPPMTA02.qualcomm.com (8.17.1.5/8.17.1.5) with ESMTPS id 3114IxUa009898 (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NOT); Wed, 1 Feb 2023 04:18:59 GMT Received: from hu-bjorande-lv.qualcomm.com (10.49.16.6) by nalasex01c.na.qualcomm.com (10.47.97.35) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.986.36; Tue, 31 Jan 2023 20:18:59 -0800 From: Bjorn Andersson To: Andy Gross , Bjorn Andersson , Konrad Dybcio , Rob Herring , Krzysztof Kozlowski , Sebastian Reichel CC: , , , , "Subbaraman Narayanamurthy" , Johan Hovold , Neil Armstrong Subject: [PATCH v4 1/4] dt-bindings: soc: qcom: Introduce PMIC GLINK binding Date: Tue, 31 Jan 2023 20:18:50 -0800 Message-ID: <20230201041853.1934355-2-quic_bjorande@quicinc.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20230201041853.1934355-1-quic_bjorande@quicinc.com> References: <20230201041853.1934355-1-quic_bjorande@quicinc.com> MIME-Version: 1.0 X-Originating-IP: [10.49.16.6] X-ClientProxiedBy: nalasex01c.na.qualcomm.com (10.47.97.35) To nalasex01c.na.qualcomm.com (10.47.97.35) X-QCInternal: smtphost X-Proofpoint-Virus-Version: vendor=nai engine=6200 definitions=5800 signatures=585085 X-Proofpoint-GUID: YbTn68O2dTx1PqzV7z5KpU9Izvi_elb5 X-Proofpoint-ORIG-GUID: YbTn68O2dTx1PqzV7z5KpU9Izvi_elb5 X-Proofpoint-Virus-Version: vendor=baseguard engine=ICAP:2.0.219,Aquarius:18.0.930,Hydra:6.0.562,FMLib:17.11.122.1 definitions=2023-02-01_01,2023-01-31_01,2022-06-22_01 X-Proofpoint-Spam-Details: rule=outbound_notspam policy=outbound score=0 bulkscore=0 impostorscore=0 mlxscore=0 suspectscore=0 adultscore=0 lowpriorityscore=0 malwarescore=0 phishscore=0 priorityscore=1501 clxscore=1015 spamscore=0 mlxlogscore=999 classifier=spam adjust=0 reason=mlx scancount=1 engine=8.12.0-2212070000 definitions=main-2302010035 Precedence: bulk List-ID: X-Mailing-List: linux-pm@vger.kernel.org From: Bjorn Andersson The PMIC GLINK service, running on a coprocessor on some modern Qualcomm platforms and implement USB Type-C handling and battery management. This binding describes the component in the OS used to communicate with the firmware and connect it's resources to those described in the Devicetree, particularly the USB Type-C controllers relationship with USB and DisplayPort components. Signed-off-by: Bjorn Andersson Tested-by: Konrad Dybcio # SM8350 PDX215 Signed-off-by: Bjorn Andersson --- Changes since v3: - Dropped the additional endpoint from port@1 after discussions with Rob. .../bindings/soc/qcom/qcom,pmic-glink.yaml | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 Documentation/devicetree/bindings/soc/qcom/qcom,pmic-glink.yaml diff --git a/Documentation/devicetree/bindings/soc/qcom/qcom,pmic-glink.yaml b/Documentation/devicetree/bindings/soc/qcom/qcom,pmic-glink.yaml new file mode 100644 index 000000000000..cf863683c21a --- /dev/null +++ b/Documentation/devicetree/bindings/soc/qcom/qcom,pmic-glink.yaml @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/soc/qcom/qcom,pmic-glink.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Qualcomm PMIC GLINK firmware interface for battery management, USB + Type-C and other things. + +maintainers: + - Bjorn Andersson + +description: + The PMIC GLINK service, running on a coprocessor on some modern Qualcomm + platforms and implement USB Type-C handling and battery management. This + binding describes the component in the OS used to communicate with the + firmware and connect it's resources to those described in the Devicetree, + particularly the USB Type-C controllers relationship with USB and DisplayPort + components. + +properties: + compatible: + items: + - enum: + - qcom,sc8180x-pmic-glink + - qcom,sc8280xp-pmic-glink + - qcom,sm8350-pmic-glink + - const: qcom,pmic-glink + + '#address-cells': + const: 1 + + '#size-cells': + const: 0 + +patternProperties: + '^connector@\d$': + $ref: /schemas/connector/usb-connector.yaml# + + properties: + reg: true + + required: + - reg + + unevaluatedProperties: false + +required: + - compatible + +additionalProperties: false + +examples: + - |+ + pmic-glink { + compatible = "qcom,sc8280xp-pmic-glink", "qcom,pmic-glink"; + + #address-cells = <1>; + #size-cells = <0>; + + connector@0 { + compatible = "usb-c-connector"; + reg = <0>; + power-role = "dual"; + data-role = "dual"; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + endpoint { + remote-endpoint = <&usb_role>; + }; + }; + + port@1 { + reg = <1>; + endpoint { + remote-endpoint = <&ss_phy_out>; + }; + }; + + port@2 { + reg = <2>; + endpoint { + remote-endpoint = <&sbu_mux>; + }; + }; + }; + }; + }; +... + From patchwork Wed Feb 1 04:18:52 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Bjorn Andersson X-Patchwork-Id: 650867 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id C588BC6379F for ; Wed, 1 Feb 2023 04:19:09 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S229500AbjBAETI (ORCPT ); Tue, 31 Jan 2023 23:19:08 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:35018 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S231329AbjBAETH (ORCPT ); Tue, 31 Jan 2023 23:19:07 -0500 Received: from mx0a-0031df01.pphosted.com (mx0a-0031df01.pphosted.com [205.220.168.131]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 228EA4A1F5; Tue, 31 Jan 2023 20:19:05 -0800 (PST) Received: from pps.filterd (m0279867.ppops.net [127.0.0.1]) by mx0a-0031df01.pphosted.com (8.17.1.19/8.17.1.19) with ESMTP id 3113cJ7n013689; Wed, 1 Feb 2023 04:19:01 GMT DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=quicinc.com; h=from : to : cc : subject : date : message-id : in-reply-to : references : mime-version : content-transfer-encoding : content-type; s=qcppdkim1; bh=12sah9jIETGrP7etc3UllRWSOugv5MqA7/SeLdzHQso=; b=QB5tkYZNE+N31HGu6CEgks7UySYAXxcvgNLz1onnC47GVlrG72MztgoFrzBvXOZeMCQv D3a8zEJgwlNbE9K1D+pH8ZXb81N/gspIRGGLGvvP3NCBUIg0nnZTMN5BbHFPQwmKxIjk OBzrUtq1XY5gWviamDivlE5fF76QUEBUAlpmVXLosQrkxQz+seMTWzkCdhQgOg49Efhv fnzBPgFzV/Hk9/PFq7532QjddSse71VoEDT4M8ovDsaWn2VDK8UQDAvKXq/1VOAY87+F sy8mYBxCvkajgxM2c7kuESU/pxWGP0qIYVVTiOK+Ec97sgh9Hg9mU6jrH7wdhC85ubTp GQ== Received: from nalasppmta03.qualcomm.com (Global_NAT1.qualcomm.com [129.46.96.20]) by mx0a-0031df01.pphosted.com (PPS) with ESMTPS id 3nexb02hds-1 (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NOT); Wed, 01 Feb 2023 04:19:00 +0000 Received: from nalasex01c.na.qualcomm.com (nalasex01c.na.qualcomm.com [10.47.97.35]) by NALASPPMTA03.qualcomm.com (8.17.1.5/8.17.1.5) with ESMTPS id 3114J0mD022458 (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NOT); Wed, 1 Feb 2023 04:19:00 GMT Received: from hu-bjorande-lv.qualcomm.com (10.49.16.6) by nalasex01c.na.qualcomm.com (10.47.97.35) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.986.36; Tue, 31 Jan 2023 20:18:59 -0800 From: Bjorn Andersson To: Andy Gross , Bjorn Andersson , Konrad Dybcio , Rob Herring , Krzysztof Kozlowski , Sebastian Reichel CC: , , , , "Subbaraman Narayanamurthy" , Johan Hovold , Neil Armstrong Subject: [PATCH v4 3/4] soc: qcom: pmic_glink: Introduce altmode support Date: Tue, 31 Jan 2023 20:18:52 -0800 Message-ID: <20230201041853.1934355-4-quic_bjorande@quicinc.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20230201041853.1934355-1-quic_bjorande@quicinc.com> References: <20230201041853.1934355-1-quic_bjorande@quicinc.com> MIME-Version: 1.0 X-Originating-IP: [10.49.16.6] X-ClientProxiedBy: nalasex01c.na.qualcomm.com (10.47.97.35) To nalasex01c.na.qualcomm.com (10.47.97.35) X-QCInternal: smtphost X-Proofpoint-Virus-Version: vendor=nai engine=6200 definitions=5800 signatures=585085 X-Proofpoint-GUID: E4NneSGRKR_BB7CtJBwv4Y5lDoMQ64DL X-Proofpoint-ORIG-GUID: E4NneSGRKR_BB7CtJBwv4Y5lDoMQ64DL X-Proofpoint-Virus-Version: vendor=baseguard engine=ICAP:2.0.219,Aquarius:18.0.930,Hydra:6.0.562,FMLib:17.11.122.1 definitions=2023-01-31_08,2023-01-31_01,2022-06-22_01 X-Proofpoint-Spam-Details: rule=outbound_notspam policy=outbound score=0 phishscore=0 mlxlogscore=999 malwarescore=0 clxscore=1015 adultscore=0 suspectscore=0 spamscore=0 bulkscore=0 priorityscore=1501 mlxscore=0 lowpriorityscore=0 impostorscore=0 classifier=spam adjust=0 reason=mlx scancount=1 engine=8.12.0-2212070000 definitions=main-2302010035 Precedence: bulk List-ID: X-Mailing-List: linux-pm@vger.kernel.org From: Bjorn Andersson With the PMIC GLINK service, the host OS subscribes to USB-C altmode messages, which are sent by the firmware to notify the host OS about state updates and HPD interrupts. The pmic_glink_altmode driver registers for these notifications and propagates the notifications as typec_mux, typec_switch and DRM OOB notifications as necessary to implement DisplayPort altmode support. Signed-off-by: Bjorn Andersson Acked-by: Neil Armstrong Tested-by: Konrad Dybcio # SM8350 PDX215 Reviewed-by: Neil Armstrong Tested-by: Neil Armstrong # on SM8550-MTP & SM8450-HDK Signed-off-by: Bjorn Andersson --- Changes since v3: - None drivers/soc/qcom/Makefile | 1 + drivers/soc/qcom/pmic_glink_altmode.c | 478 ++++++++++++++++++++++++++ 2 files changed, 479 insertions(+) create mode 100644 drivers/soc/qcom/pmic_glink_altmode.c diff --git a/drivers/soc/qcom/Makefile b/drivers/soc/qcom/Makefile index 29cccac472f3..f30552bf4da7 100644 --- a/drivers/soc/qcom/Makefile +++ b/drivers/soc/qcom/Makefile @@ -10,6 +10,7 @@ obj-$(CONFIG_QCOM_MDT_LOADER) += mdt_loader.o obj-$(CONFIG_QCOM_OCMEM) += ocmem.o obj-$(CONFIG_QCOM_PDR_HELPERS) += pdr_interface.o obj-$(CONFIG_QCOM_PMIC_GLINK) += pmic_glink.o +obj-$(CONFIG_QCOM_PMIC_GLINK) += pmic_glink_altmode.o obj-$(CONFIG_QCOM_QMI_HELPERS) += qmi_helpers.o qmi_helpers-y += qmi_encdec.o qmi_interface.o obj-$(CONFIG_QCOM_RAMP_CTRL) += ramp_controller.o diff --git a/drivers/soc/qcom/pmic_glink_altmode.c b/drivers/soc/qcom/pmic_glink_altmode.c new file mode 100644 index 000000000000..71f861b8cb51 --- /dev/null +++ b/drivers/soc/qcom/pmic_glink_altmode.c @@ -0,0 +1,478 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2019-2020, The Linux Foundation. All rights reserved. + * Copyright (c) 2022, Linaro Ltd + */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#define PMIC_GLINK_MAX_PORTS 2 + +#define USBC_SC8180X_NOTIFY_IND 0x13 +#define USBC_CMD_WRITE_REQ 0x15 +#define USBC_NOTIFY_IND 0x16 + +#define ALTMODE_PAN_EN 0x10 +#define ALTMODE_PAN_ACK 0x11 + +struct usbc_write_req { + struct pmic_glink_hdr hdr; + __le32 cmd; + __le32 arg; + __le32 reserved; +}; + +#define NOTIFY_PAYLOAD_SIZE 16 +struct usbc_notify { + struct pmic_glink_hdr hdr; + char payload[NOTIFY_PAYLOAD_SIZE]; + u32 reserved; +}; + +struct usbc_sc8180x_notify { + struct pmic_glink_hdr hdr; + __le32 notification; + __le32 reserved[2]; +}; + +enum pmic_glink_altmode_pin_assignment { + DPAM_HPD_OUT, + DPAM_HPD_A, + DPAM_HPD_B, + DPAM_HPD_C, + DPAM_HPD_D, + DPAM_HPD_E, + DPAM_HPD_F, +}; + +struct pmic_glink_altmode; + +#define work_to_altmode_port(w) container_of((w), struct pmic_glink_altmode_port, work) + +struct pmic_glink_altmode_port { + struct pmic_glink_altmode *altmode; + unsigned int index; + + struct typec_switch *typec_switch; + struct typec_mux *typec_mux; + struct typec_mux_state state; + struct typec_altmode dp_alt; + + struct work_struct work; + + struct drm_bridge bridge; + + enum typec_orientation orientation; + u16 svid; + u8 dp_data; + u8 mode; + u8 hpd_state; + u8 hpd_irq; +}; + +#define work_to_altmode(w) container_of((w), struct pmic_glink_altmode, enable_work) + +struct pmic_glink_altmode { + struct device *dev; + + unsigned int owner_id; + + /* To synchronize WRITE_REQ acks */ + struct mutex lock; + + struct completion pan_ack; + struct pmic_glink_client *client; + + struct work_struct enable_work; + + struct pmic_glink_altmode_port ports[PMIC_GLINK_MAX_PORTS]; +}; + +static int pmic_glink_altmode_request(struct pmic_glink_altmode *altmode, u32 cmd, u32 arg) +{ + struct usbc_write_req req = {}; + unsigned long left; + int ret; + + /* + * The USBC_CMD_WRITE_REQ ack doesn't identify the request, so wait for + * one ack at a time. + */ + mutex_lock(&altmode->lock); + + req.hdr.owner = cpu_to_le32(altmode->owner_id); + req.hdr.type = cpu_to_le32(PMIC_GLINK_REQ_RESP); + req.hdr.opcode = cpu_to_le32(USBC_CMD_WRITE_REQ); + req.cmd = cpu_to_le32(cmd); + req.arg = cpu_to_le32(arg); + + ret = pmic_glink_send(altmode->client, &req, sizeof(req)); + if (ret) { + dev_err(altmode->dev, "failed to send altmode request: %#x (%d)\n", cmd, ret); + goto out_unlock; + } + + left = wait_for_completion_timeout(&altmode->pan_ack, 5 * HZ); + if (!left) { + dev_err(altmode->dev, "timeout waiting for altmode request ack for: %#x\n", cmd); + ret = -ETIMEDOUT; + } + +out_unlock: + mutex_unlock(&altmode->lock); + return ret; +} + +static void pmic_glink_altmode_enable_dp(struct pmic_glink_altmode *altmode, + struct pmic_glink_altmode_port *port, + u8 mode, bool hpd_state, + bool hpd_irq) +{ + struct typec_displayport_data dp_data = {}; + int ret; + + dp_data.status = DP_STATUS_ENABLED; + if (hpd_state) + dp_data.status |= DP_STATUS_HPD_STATE; + if (hpd_irq) + dp_data.status |= DP_STATUS_IRQ_HPD; + dp_data.conf = DP_CONF_SET_PIN_ASSIGN(mode); + + port->state.alt = &port->dp_alt; + port->state.data = &dp_data; + port->state.mode = TYPEC_MODAL_STATE(mode); + + ret = typec_mux_set(port->typec_mux, &port->state); + if (ret) + dev_err(altmode->dev, "failed to switch mux to DP\n"); +} + +static void pmic_glink_altmode_enable_usb(struct pmic_glink_altmode *altmode, + struct pmic_glink_altmode_port *port) +{ + int ret; + + port->state.alt = NULL; + port->state.data = NULL; + port->state.mode = TYPEC_STATE_USB; + + ret = typec_mux_set(port->typec_mux, &port->state); + if (ret) + dev_err(altmode->dev, "failed to switch mux to USB\n"); +} + +static void pmic_glink_altmode_worker(struct work_struct *work) +{ + struct pmic_glink_altmode_port *alt_port = work_to_altmode_port(work); + struct pmic_glink_altmode *altmode = alt_port->altmode; + + typec_switch_set(alt_port->typec_switch, alt_port->orientation); + + if (alt_port->svid == USB_TYPEC_DP_SID) + pmic_glink_altmode_enable_dp(altmode, alt_port, alt_port->mode, + alt_port->hpd_state, alt_port->hpd_irq); + else + pmic_glink_altmode_enable_usb(altmode, alt_port); + + if (alt_port->hpd_state) + drm_bridge_hpd_notify(&alt_port->bridge, connector_status_connected); + else + drm_bridge_hpd_notify(&alt_port->bridge, connector_status_disconnected); + + pmic_glink_altmode_request(altmode, ALTMODE_PAN_ACK, alt_port->index); +}; + +static enum typec_orientation pmic_glink_altmode_orientation(unsigned int orientation) +{ + if (orientation == 0) + return TYPEC_ORIENTATION_NORMAL; + else if (orientation == 1) + return TYPEC_ORIENTATION_REVERSE; + else + return TYPEC_ORIENTATION_NONE; +} + +#define SC8180X_PORT_MASK 0x000000ff +#define SC8180X_ORIENTATION_MASK 0x0000ff00 +#define SC8180X_MUX_MASK 0x00ff0000 +#define SC8180X_MODE_MASK 0x3f000000 +#define SC8180X_HPD_STATE_MASK 0x40000000 +#define SC8180X_HPD_IRQ_MASK 0x80000000 + +static void pmic_glink_altmode_sc8180xp_notify(struct pmic_glink_altmode *altmode, + const void *data, size_t len) +{ + struct pmic_glink_altmode_port *alt_port; + const struct usbc_sc8180x_notify *msg; + u32 notification; + u8 orientation; + u8 hpd_state; + u8 hpd_irq; + u16 svid; + u8 port; + u8 mode; + u8 mux; + + if (len != sizeof(*msg)) { + dev_warn(altmode->dev, "invalid length of USBC_NOTIFY indication: %zd\n", len); + return; + } + + msg = data; + notification = le32_to_cpu(msg->notification); + port = FIELD_GET(SC8180X_PORT_MASK, notification); + orientation = FIELD_GET(SC8180X_ORIENTATION_MASK, notification); + mux = FIELD_GET(SC8180X_MUX_MASK, notification); + mode = FIELD_GET(SC8180X_MODE_MASK, notification); + hpd_state = FIELD_GET(SC8180X_HPD_STATE_MASK, notification); + hpd_irq = FIELD_GET(SC8180X_HPD_IRQ_MASK, notification); + + svid = mux == 2 ? USB_TYPEC_DP_SID : 0; + + if (!altmode->ports[port].altmode) { + dev_dbg(altmode->dev, "notification on undefined port %d\n", port); + return; + } + + alt_port = &altmode->ports[port]; + alt_port->orientation = pmic_glink_altmode_orientation(orientation); + alt_port->svid = mux == 2 ? USB_TYPEC_DP_SID : 0; + alt_port->mode = mode; + alt_port->hpd_state = hpd_state; + alt_port->hpd_irq = hpd_irq; + schedule_work(&alt_port->work); +} + +#define SC8280XP_DPAM_MASK 0x3f +#define SC8280XP_HPD_STATE_MASK BIT(6) +#define SC8280XP_HPD_IRQ_MASK BIT(7) + +static void pmic_glink_altmode_sc8280xp_notify(struct pmic_glink_altmode *altmode, + u16 svid, const void *data, size_t len) +{ + struct pmic_glink_altmode_port *alt_port; + const struct usbc_notify *notify; + u8 orientation; + u8 hpd_state; + u8 hpd_irq; + u8 mode; + u8 port; + + if (len != sizeof(*notify)) { + dev_warn(altmode->dev, "invalid length USBC_NOTIFY_IND: %zd\n", + len); + return; + } + + notify = data; + + port = notify->payload[0]; + orientation = notify->payload[1]; + mode = FIELD_GET(SC8280XP_DPAM_MASK, notify->payload[8]) - DPAM_HPD_A; + hpd_state = FIELD_GET(SC8280XP_HPD_STATE_MASK, notify->payload[8]); + hpd_irq = FIELD_GET(SC8280XP_HPD_IRQ_MASK, notify->payload[8]); + + if (!altmode->ports[port].altmode) { + dev_dbg(altmode->dev, "notification on undefined port %d\n", port); + return; + } + + alt_port = &altmode->ports[port]; + alt_port->orientation = pmic_glink_altmode_orientation(orientation); + alt_port->svid = svid; + alt_port->mode = mode; + alt_port->hpd_state = hpd_state; + alt_port->hpd_irq = hpd_irq; + schedule_work(&alt_port->work); +} + +static void pmic_glink_altmode_callback(const void *data, size_t len, void *priv) +{ + struct pmic_glink_altmode *altmode = priv; + const struct pmic_glink_hdr *hdr = data; + u16 opcode; + u16 svid; + + opcode = le32_to_cpu(hdr->opcode) & 0xff; + svid = le32_to_cpu(hdr->opcode) >> 16; + + switch (opcode) { + case USBC_CMD_WRITE_REQ: + complete(&altmode->pan_ack); + break; + case USBC_NOTIFY_IND: + pmic_glink_altmode_sc8280xp_notify(altmode, svid, data, len); + break; + case USBC_SC8180X_NOTIFY_IND: + pmic_glink_altmode_sc8180xp_notify(altmode, data, len); + break; + } +} + +static int pmic_glink_altmode_attach(struct drm_bridge *bridge, + enum drm_bridge_attach_flags flags) +{ + return flags & DRM_BRIDGE_ATTACH_NO_CONNECTOR ? 0 : -EINVAL; +} + +static const struct drm_bridge_funcs pmic_glink_altmode_bridge_funcs = { + .attach = pmic_glink_altmode_attach, +}; + +static void pmic_glink_altmode_put_mux(void *data) +{ + typec_mux_put(data); +} + +static void pmic_glink_altmode_put_switch(void *data) +{ + typec_switch_put(data); +} + +static void pmic_glink_altmode_enable_worker(struct work_struct *work) +{ + struct pmic_glink_altmode *altmode = work_to_altmode(work); + int ret; + + ret = pmic_glink_altmode_request(altmode, ALTMODE_PAN_EN, 0); + if (ret) + dev_err(altmode->dev, "failed to request altmode notifications\n"); +} + +static void pmic_glink_altmode_pdr_notify(void *priv, int state) +{ + struct pmic_glink_altmode *altmode = priv; + + if (state == SERVREG_SERVICE_STATE_UP) + schedule_work(&altmode->enable_work); +} + +static const struct of_device_id pmic_glink_altmode_of_quirks[] = { + { .compatible = "qcom,sc8180x-pmic-glink", .data = (void *)PMIC_GLINK_OWNER_USBC }, + {} +}; + +static int pmic_glink_altmode_probe(struct auxiliary_device *adev, + const struct auxiliary_device_id *id) +{ + struct pmic_glink_altmode_port *alt_port; + struct pmic_glink_altmode *altmode; + struct typec_altmode_desc mux_desc = {}; + const struct of_device_id *match; + struct fwnode_handle *fwnode; + struct device *dev = &adev->dev; + u32 port; + int ret; + + altmode = devm_kzalloc(dev, sizeof(*altmode), GFP_KERNEL); + if (!altmode) + return -ENOMEM; + + altmode->dev = dev; + + match = of_match_device(pmic_glink_altmode_of_quirks, dev->parent); + if (match) + altmode->owner_id = (unsigned long)match->data; + else + altmode->owner_id = PMIC_GLINK_OWNER_USBC_PAN; + + INIT_WORK(&altmode->enable_work, pmic_glink_altmode_enable_worker); + init_completion(&altmode->pan_ack); + mutex_init(&altmode->lock); + + device_for_each_child_node(dev, fwnode) { + ret = fwnode_property_read_u32(fwnode, "reg", &port); + if (ret < 0) { + dev_err(dev, "missing reg property of %pOFn\n", fwnode); + return ret; + } + + if (port >= ARRAY_SIZE(altmode->ports)) { + dev_warn(dev, "invalid connector number, ignoring\n"); + continue; + } + + if (altmode->ports[port].altmode) { + dev_err(dev, "multiple connector definition for port %u\n", port); + return -EINVAL; + } + + alt_port = &altmode->ports[port]; + alt_port->altmode = altmode; + alt_port->index = port; + INIT_WORK(&alt_port->work, pmic_glink_altmode_worker); + + alt_port->bridge.funcs = &pmic_glink_altmode_bridge_funcs; + alt_port->bridge.of_node = to_of_node(fwnode); + alt_port->bridge.ops = DRM_BRIDGE_OP_HPD; + alt_port->bridge.type = DRM_MODE_CONNECTOR_USB; + + ret = devm_drm_bridge_add(dev, &alt_port->bridge); + if (ret) + return ret; + + alt_port->dp_alt.svid = USB_TYPEC_DP_SID; + alt_port->dp_alt.mode = USB_TYPEC_DP_MODE; + alt_port->dp_alt.active = 1; + + mux_desc.svid = USB_TYPEC_DP_SID; + mux_desc.mode = USB_TYPEC_DP_MODE; + alt_port->typec_mux = fwnode_typec_mux_get(fwnode, &mux_desc); + if (IS_ERR(alt_port->typec_mux)) + return dev_err_probe(dev, PTR_ERR(alt_port->typec_mux), + "failed to acquire mode-switch for port: %d\n", + port); + + ret = devm_add_action_or_reset(dev, pmic_glink_altmode_put_mux, + alt_port->typec_mux); + if (ret) + return ret; + + alt_port->typec_switch = fwnode_typec_switch_get(fwnode); + if (IS_ERR(alt_port->typec_switch)) + return dev_err_probe(dev, PTR_ERR(alt_port->typec_switch), + "failed to acquire orientation-switch for port: %d\n", + port); + + ret = devm_add_action_or_reset(dev, pmic_glink_altmode_put_switch, + alt_port->typec_switch); + if (ret) + return ret; + } + + altmode->client = devm_pmic_glink_register_client(dev, + altmode->owner_id, + pmic_glink_altmode_callback, + pmic_glink_altmode_pdr_notify, + altmode); + return PTR_ERR_OR_ZERO(altmode->client); +} + +static const struct auxiliary_device_id pmic_glink_altmode_id_table[] = { + { .name = "pmic_glink.altmode", }, + {}, +}; +MODULE_DEVICE_TABLE(auxiliary, pmic_glink_altmode_id_table); + +static struct auxiliary_driver pmic_glink_altmode_driver = { + .name = "pmic_glink_altmode", + .probe = pmic_glink_altmode_probe, + .id_table = pmic_glink_altmode_id_table, +}; + +module_auxiliary_driver(pmic_glink_altmode_driver); + +MODULE_DESCRIPTION("Qualcomm PMIC GLINK Altmode driver"); +MODULE_LICENSE("GPL"); From patchwork Wed Feb 1 04:18:53 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Bjorn Andersson X-Patchwork-Id: 650866 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 1B040C6379F for ; Wed, 1 Feb 2023 04:19:15 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S231305AbjBAETM (ORCPT ); Tue, 31 Jan 2023 23:19:12 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:35162 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S231154AbjBAETL (ORCPT ); Tue, 31 Jan 2023 23:19:11 -0500 Received: from mx0b-0031df01.pphosted.com (mx0b-0031df01.pphosted.com [205.220.180.131]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 3F39A58295; Tue, 31 Jan 2023 20:19:07 -0800 (PST) Received: from pps.filterd (m0279871.ppops.net [127.0.0.1]) by mx0a-0031df01.pphosted.com (8.17.1.19/8.17.1.19) with ESMTP id 3113rgp7025891; Wed, 1 Feb 2023 04:19:02 GMT DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=quicinc.com; h=from : to : cc : subject : date : message-id : in-reply-to : references : mime-version : content-transfer-encoding : content-type; s=qcppdkim1; bh=YNtdeUuWflpp2D6vQh+RHaYl0ZDqeu/4m4NMXx3BwWs=; b=PnPl2uaA4xY6KBxLefFsmhPLjYTW7FTri+92l4l1Cg+6SamZ+qIsK3ov7iY1i9dMhRo0 LQ4cZNDHGwPDtmB6LiHItOZJrZqc/YLDewGaiHKPQlB29FzqtjmZjuW4qHokICLgNwtc i/UkYuYn7dYXUaBDeEBq9zKPXxUcQFL54/SYsKb18jzBixYzZanOEeowT+kgFMnBdHSY t7dF0wU4occc7OELDZaOUVvx3x0dKLHNABsoYa1JopBpz3kbosN6MzcRovXBx92zxKXJ bpsNsae5jpfCt7G1st1+LumKHQ6OgRs+Xn2mXOQfLpdvUbg19D6Z7iXmwKd4NnZsTCLJ Lw== Received: from nalasppmta01.qualcomm.com (Global_NAT1.qualcomm.com [129.46.96.20]) by mx0a-0031df01.pphosted.com (PPS) with ESMTPS id 3new3uaxcn-1 (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NOT); Wed, 01 Feb 2023 04:19:01 +0000 Received: from nalasex01c.na.qualcomm.com (nalasex01c.na.qualcomm.com [10.47.97.35]) by NALASPPMTA01.qualcomm.com (8.17.1.5/8.17.1.5) with ESMTPS id 3114J0uu004547 (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NOT); Wed, 1 Feb 2023 04:19:00 GMT Received: from hu-bjorande-lv.qualcomm.com (10.49.16.6) by nalasex01c.na.qualcomm.com (10.47.97.35) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.986.36; Tue, 31 Jan 2023 20:18:59 -0800 From: Bjorn Andersson To: Andy Gross , Bjorn Andersson , Konrad Dybcio , Rob Herring , Krzysztof Kozlowski , Sebastian Reichel CC: , , , , "Subbaraman Narayanamurthy" , Johan Hovold , Neil Armstrong Subject: [PATCH v4 4/4] power: supply: Introduce Qualcomm PMIC GLINK power supply Date: Tue, 31 Jan 2023 20:18:53 -0800 Message-ID: <20230201041853.1934355-5-quic_bjorande@quicinc.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20230201041853.1934355-1-quic_bjorande@quicinc.com> References: <20230201041853.1934355-1-quic_bjorande@quicinc.com> MIME-Version: 1.0 X-Originating-IP: [10.49.16.6] X-ClientProxiedBy: nalasex01c.na.qualcomm.com (10.47.97.35) To nalasex01c.na.qualcomm.com (10.47.97.35) X-QCInternal: smtphost X-Proofpoint-Virus-Version: vendor=nai engine=6200 definitions=5800 signatures=585085 X-Proofpoint-GUID: AhW9XzkN2C6ViZ4TcMN-LZO0CZbrhSXh X-Proofpoint-ORIG-GUID: AhW9XzkN2C6ViZ4TcMN-LZO0CZbrhSXh X-Proofpoint-Virus-Version: vendor=baseguard engine=ICAP:2.0.219,Aquarius:18.0.930,Hydra:6.0.562,FMLib:17.11.122.1 definitions=2023-01-31_08,2023-01-31_01,2022-06-22_01 X-Proofpoint-Spam-Details: rule=outbound_notspam policy=outbound score=0 lowpriorityscore=0 phishscore=0 clxscore=1015 suspectscore=0 mlxscore=0 mlxlogscore=999 impostorscore=0 adultscore=0 bulkscore=0 malwarescore=0 priorityscore=1501 spamscore=0 classifier=spam adjust=0 reason=mlx scancount=1 engine=8.12.0-2212070000 definitions=main-2302010035 Precedence: bulk List-ID: X-Mailing-List: linux-pm@vger.kernel.org From: Bjorn Andersson The PMIC GLINK service, running on a coprocessor of modern Qualcomm platforms, deals with battery charging and fuel gauging, as well as reporting status of AC and wireless power supplies. As this is just one of the functionalities provided by the PMIC GLINK service, this power supply driver is implemented as an auxilirary bus driver, spawned by the main "pmic glink" driver when the PMIC GLINK service is detected. Signed-off-by: Bjorn Andersson Tested-by: Konrad Dybcio # SM8350 PDX215 Reviewed-by: Neil Armstrong Tested-by: Neil Armstrong # on SM8550-MTP & SM8450-HDK Signed-off-by: Bjorn Andersson --- Changes since v3: - None drivers/power/supply/Kconfig | 9 + drivers/power/supply/Makefile | 1 + drivers/power/supply/qcom_battmgr.c | 1421 +++++++++++++++++++++++++++ 3 files changed, 1431 insertions(+) create mode 100644 drivers/power/supply/qcom_battmgr.c diff --git a/drivers/power/supply/Kconfig b/drivers/power/supply/Kconfig index e2f8dfcdd2a9..a2292f19d301 100644 --- a/drivers/power/supply/Kconfig +++ b/drivers/power/supply/Kconfig @@ -174,6 +174,15 @@ config BATTERY_PMU Say Y here to expose battery information on Apple machines through the generic battery class. +config BATTERY_QCOM_BATTMGR + tristate "Qualcomm PMIC GLINK battery manager support" + depends on QCOM_PMIC_GLINK + select AUXILIARY_BUS + help + Say Y here to enable the Qualcomm PMIC GLINK power supply driver, + which is used on modern Qualcomm platforms to provide battery and + power supply information. + config BATTERY_OLPC tristate "One Laptop Per Child battery" depends on OLPC_EC diff --git a/drivers/power/supply/Makefile b/drivers/power/supply/Makefile index 8cb3c7f5c111..dc4a3a58a5c0 100644 --- a/drivers/power/supply/Makefile +++ b/drivers/power/supply/Makefile @@ -33,6 +33,7 @@ obj-$(CONFIG_BATTERY_GAUGE_LTC2941) += ltc2941-battery-gauge.o obj-$(CONFIG_BATTERY_GOLDFISH) += goldfish_battery.o obj-$(CONFIG_BATTERY_LEGO_EV3) += lego_ev3_battery.o obj-$(CONFIG_BATTERY_PMU) += pmu_battery.o +obj-$(CONFIG_BATTERY_QCOM_BATTMGR) += qcom_battmgr.o obj-$(CONFIG_BATTERY_OLPC) += olpc_battery.o obj-$(CONFIG_BATTERY_SAMSUNG_SDI) += samsung-sdi-battery.o obj-$(CONFIG_BATTERY_COLLIE) += collie_battery.o diff --git a/drivers/power/supply/qcom_battmgr.c b/drivers/power/supply/qcom_battmgr.c new file mode 100644 index 000000000000..ed169b47c538 --- /dev/null +++ b/drivers/power/supply/qcom_battmgr.c @@ -0,0 +1,1421 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2019-2020, The Linux Foundation. All rights reserved. + * Copyright (c) 2022, Linaro Ltd + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define BATTMGR_CHEMISTRY_LEN 4 +#define BATTMGR_STRING_LEN 128 + +enum qcom_battmgr_variant { + QCOM_BATTMGR_SM8350, + QCOM_BATTMGR_SC8280XP, +}; + +#define BATTMGR_BAT_STATUS 0x1 + +#define BATTMGR_REQUEST_NOTIFICATION 0x4 + +#define BATTMGR_NOTIFICATION 0x7 +#define NOTIF_BAT_PROPERTY 0x30 +#define NOTIF_USB_PROPERTY 0x32 +#define NOTIF_WLS_PROPERTY 0x34 +#define NOTIF_BAT_INFO 0x81 +#define NOTIF_BAT_STATUS 0x80 + +#define BATTMGR_BAT_INFO 0x9 + +#define BATTMGR_BAT_DISCHARGE_TIME 0xc + +#define BATTMGR_BAT_CHARGE_TIME 0xd + +#define BATTMGR_BAT_PROPERTY_GET 0x30 +#define BATTMGR_BAT_PROPERTY_SET 0x31 +#define BATT_STATUS 0 +#define BATT_HEALTH 1 +#define BATT_PRESENT 2 +#define BATT_CHG_TYPE 3 +#define BATT_CAPACITY 4 +#define BATT_SOH 5 +#define BATT_VOLT_OCV 6 +#define BATT_VOLT_NOW 7 +#define BATT_VOLT_MAX 8 +#define BATT_CURR_NOW 9 +#define BATT_CHG_CTRL_LIM 10 +#define BATT_CHG_CTRL_LIM_MAX 11 +#define BATT_TEMP 12 +#define BATT_TECHNOLOGY 13 +#define BATT_CHG_COUNTER 14 +#define BATT_CYCLE_COUNT 15 +#define BATT_CHG_FULL_DESIGN 16 +#define BATT_CHG_FULL 17 +#define BATT_MODEL_NAME 18 +#define BATT_TTF_AVG 19 +#define BATT_TTE_AVG 20 +#define BATT_RESISTANCE 21 +#define BATT_POWER_NOW 22 +#define BATT_POWER_AVG 23 + +#define BATTMGR_USB_PROPERTY_GET 0x32 +#define BATTMGR_USB_PROPERTY_SET 0x33 +#define USB_ONLINE 0 +#define USB_VOLT_NOW 1 +#define USB_VOLT_MAX 2 +#define USB_CURR_NOW 3 +#define USB_CURR_MAX 4 +#define USB_INPUT_CURR_LIMIT 5 +#define USB_TYPE 6 +#define USB_ADAP_TYPE 7 +#define USB_MOISTURE_DET_EN 8 +#define USB_MOISTURE_DET_STS 9 + +#define BATTMGR_WLS_PROPERTY_GET 0x34 +#define BATTMGR_WLS_PROPERTY_SET 0x35 +#define WLS_ONLINE 0 +#define WLS_VOLT_NOW 1 +#define WLS_VOLT_MAX 2 +#define WLS_CURR_NOW 3 +#define WLS_CURR_MAX 4 +#define WLS_TYPE 5 +#define WLS_BOOST_EN 6 + +struct qcom_battmgr_enable_request { + struct pmic_glink_hdr hdr; + __le32 battery_id; + __le32 power_state; + __le32 low_capacity; + __le32 high_capacity; +}; + +struct qcom_battmgr_property_request { + struct pmic_glink_hdr hdr; + __le32 battery; + __le32 property; + __le32 value; +}; + +struct qcom_battmgr_update_request { + struct pmic_glink_hdr hdr; + u32 battery_id; +}; + +struct qcom_battmgr_charge_time_request { + struct pmic_glink_hdr hdr; + __le32 battery_id; + __le32 percent; + __le32 reserved; +}; + +struct qcom_battmgr_discharge_time_request { + struct pmic_glink_hdr hdr; + __le32 battery_id; + __le32 rate; /* 0 for current rate */ + __le32 reserved; +}; + +struct qcom_battmgr_message { + struct pmic_glink_hdr hdr; + union { + struct { + __le32 property; + __le32 value; + __le32 result; + } intval; + struct { + __le32 property; + char model[BATTMGR_STRING_LEN]; + } strval; + struct { + /* + * 0: mWh + * 1: mAh + */ + __le32 power_unit; + __le32 design_capacity; + __le32 last_full_capacity; + /* + * 0 nonrechargable + * 1 rechargable + */ + __le32 battery_tech; + __le32 design_voltage; /* mV */ + __le32 capacity_low; + __le32 capacity_warning; + __le32 cycle_count; + /* thousandth of persent */ + __le32 accuracy; + __le32 max_sample_time_ms; + __le32 min_sample_time_ms; + __le32 max_average_interval_ms; + __le32 min_average_interval_ms; + /* granularity between low and warning */ + __le32 capacity_granularity1; + /* granularity between warning and full */ + __le32 capacity_granularity2; + /* + * 0: no + * 1: cold + * 2: hot + */ + __le32 swappable; + __le32 capabilities; + char model_number[BATTMGR_STRING_LEN]; + char serial_number[BATTMGR_STRING_LEN]; + char battery_type[BATTMGR_STRING_LEN]; + char oem_info[BATTMGR_STRING_LEN]; + char battery_chemistry[BATTMGR_CHEMISTRY_LEN]; + char uid[BATTMGR_STRING_LEN]; + __le32 critical_bias; + u8 day; + u8 month; + __le16 year; + __le32 battery_id; + } info; + struct { + /* + * BIT(0) discharging + * BIT(1) charging + * BIT(2) critical low + */ + __le32 battery_state; + /* mWh or mAh, based on info->power_unit */ + __le32 capacity; + __le32 rate; + /* mv */ + __le32 battery_voltage; + /* + * BIT(0) power online + * BIT(1) discharging + * BIT(2) charging + * BIT(3) battery critical + */ + __le32 power_state; + /* + * 1: AC + * 2: USB + * 3: Wireless + */ + __le32 charging_source; + __le32 temperature; + } status; + __le32 time; + __le32 notification; + }; +}; + +#define BATTMGR_CHARGING_SOURCE_AC 1 +#define BATTMGR_CHARGING_SOURCE_USB 2 +#define BATTMGR_CHARGING_SOURCE_WIRELESS 3 + +enum qcom_battmgr_unit { + QCOM_BATTMGR_UNIT_mWh = 0, + QCOM_BATTMGR_UNIT_mAh = 1 +}; + +struct qcom_battmgr_info { + bool valid; + + bool present; + unsigned int charge_type; + unsigned int design_capacity; + unsigned int last_full_capacity; + unsigned int voltage_max_design; + unsigned int voltage_max; + unsigned int capacity_low; + unsigned int capacity_warning; + unsigned int cycle_count; + unsigned int charge_count; + char model_number[BATTMGR_STRING_LEN]; + char serial_number[BATTMGR_STRING_LEN]; + char oem_info[BATTMGR_STRING_LEN]; + unsigned char technology; + unsigned char day; + unsigned char month; + unsigned short year; +}; + +struct qcom_battmgr_status { + unsigned int status; + unsigned int health; + unsigned int capacity; + unsigned int percent; + int current_now; + int power_now; + unsigned int voltage_now; + unsigned int voltage_ocv; + unsigned int temperature; + + unsigned int discharge_time; + unsigned int charge_time; +}; + +struct qcom_battmgr_ac { + bool online; +}; + +struct qcom_battmgr_usb { + bool online; + unsigned int voltage_now; + unsigned int voltage_max; + unsigned int current_now; + unsigned int current_max; + unsigned int current_limit; + unsigned int usb_type; +}; + +struct qcom_battmgr_wireless { + bool online; + unsigned int voltage_now; + unsigned int voltage_max; + unsigned int current_now; + unsigned int current_max; +}; + +struct qcom_battmgr { + struct device *dev; + struct pmic_glink_client *client; + + enum qcom_battmgr_variant variant; + + struct power_supply *ac_psy; + struct power_supply *bat_psy; + struct power_supply *usb_psy; + struct power_supply *wls_psy; + + enum qcom_battmgr_unit unit; + + int error; + struct completion ack; + + bool service_up; + + struct qcom_battmgr_info info; + struct qcom_battmgr_status status; + struct qcom_battmgr_ac ac; + struct qcom_battmgr_usb usb; + struct qcom_battmgr_wireless wireless; + + struct work_struct enable_work; + + /* + * @lock is used to prevent concurrent power supply requests to the + * firmware, as it then stops responding. + */ + struct mutex lock; +}; + +static int qcom_battmgr_request(struct qcom_battmgr *battmgr, void *data, size_t len) +{ + unsigned long left; + int ret; + + reinit_completion(&battmgr->ack); + + battmgr->error = 0; + + ret = pmic_glink_send(battmgr->client, data, len); + if (ret < 0) + return ret; + + left = wait_for_completion_timeout(&battmgr->ack, HZ); + if (!left) + return -ETIMEDOUT; + + return battmgr->error; +} + +static int qcom_battmgr_request_property(struct qcom_battmgr *battmgr, int opcode, + int property, u32 value) +{ + struct qcom_battmgr_property_request request = { + .hdr.owner = cpu_to_le32(PMIC_GLINK_OWNER_BATTMGR), + .hdr.type = cpu_to_le32(PMIC_GLINK_REQ_RESP), + .hdr.opcode = cpu_to_le32(opcode), + .battery = cpu_to_le32(0), + .property = cpu_to_le32(property), + .value = cpu_to_le32(value), + }; + + return qcom_battmgr_request(battmgr, &request, sizeof(request)); +} + +static int qcom_battmgr_update_status(struct qcom_battmgr *battmgr) +{ + struct qcom_battmgr_update_request request = { + .hdr.owner = cpu_to_le32(PMIC_GLINK_OWNER_BATTMGR), + .hdr.type = cpu_to_le32(PMIC_GLINK_REQ_RESP), + .hdr.opcode = cpu_to_le32(BATTMGR_BAT_STATUS), + .battery_id = cpu_to_le32(0), + }; + + return qcom_battmgr_request(battmgr, &request, sizeof(request)); +} + +static int qcom_battmgr_update_info(struct qcom_battmgr *battmgr) +{ + struct qcom_battmgr_update_request request = { + .hdr.owner = cpu_to_le32(PMIC_GLINK_OWNER_BATTMGR), + .hdr.type = cpu_to_le32(PMIC_GLINK_REQ_RESP), + .hdr.opcode = cpu_to_le32(BATTMGR_BAT_INFO), + .battery_id = cpu_to_le32(0), + }; + + return qcom_battmgr_request(battmgr, &request, sizeof(request)); +} + +static int qcom_battmgr_update_charge_time(struct qcom_battmgr *battmgr) +{ + struct qcom_battmgr_charge_time_request request = { + .hdr.owner = cpu_to_le32(PMIC_GLINK_OWNER_BATTMGR), + .hdr.type = cpu_to_le32(PMIC_GLINK_REQ_RESP), + .hdr.opcode = cpu_to_le32(BATTMGR_BAT_CHARGE_TIME), + .battery_id = cpu_to_le32(0), + .percent = cpu_to_le32(100), + }; + + return qcom_battmgr_request(battmgr, &request, sizeof(request)); +} + +static int qcom_battmgr_update_discharge_time(struct qcom_battmgr *battmgr) +{ + struct qcom_battmgr_discharge_time_request request = { + .hdr.owner = cpu_to_le32(PMIC_GLINK_OWNER_BATTMGR), + .hdr.type = cpu_to_le32(PMIC_GLINK_REQ_RESP), + .hdr.opcode = cpu_to_le32(BATTMGR_BAT_DISCHARGE_TIME), + .battery_id = cpu_to_le32(0), + .rate = cpu_to_le32(0), + }; + + return qcom_battmgr_request(battmgr, &request, sizeof(request)); +} + +static const u8 sm8350_bat_prop_map[] = { + [POWER_SUPPLY_PROP_STATUS] = BATT_STATUS, + [POWER_SUPPLY_PROP_HEALTH] = BATT_HEALTH, + [POWER_SUPPLY_PROP_PRESENT] = BATT_PRESENT, + [POWER_SUPPLY_PROP_CHARGE_TYPE] = BATT_CHG_TYPE, + [POWER_SUPPLY_PROP_CAPACITY] = BATT_CAPACITY, + [POWER_SUPPLY_PROP_VOLTAGE_OCV] = BATT_VOLT_OCV, + [POWER_SUPPLY_PROP_VOLTAGE_NOW] = BATT_VOLT_NOW, + [POWER_SUPPLY_PROP_VOLTAGE_MAX] = BATT_VOLT_MAX, + [POWER_SUPPLY_PROP_CURRENT_NOW] = BATT_CURR_NOW, + [POWER_SUPPLY_PROP_TEMP] = BATT_TEMP, + [POWER_SUPPLY_PROP_TECHNOLOGY] = BATT_TECHNOLOGY, + [POWER_SUPPLY_PROP_CHARGE_COUNTER] = BATT_CHG_COUNTER, + [POWER_SUPPLY_PROP_CYCLE_COUNT] = BATT_CYCLE_COUNT, + [POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN] = BATT_CHG_FULL_DESIGN, + [POWER_SUPPLY_PROP_CHARGE_FULL] = BATT_CHG_FULL, + [POWER_SUPPLY_PROP_MODEL_NAME] = BATT_MODEL_NAME, + [POWER_SUPPLY_PROP_TIME_TO_FULL_AVG] = BATT_TTF_AVG, + [POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG] = BATT_TTE_AVG, + [POWER_SUPPLY_PROP_POWER_NOW] = BATT_POWER_NOW, +}; + +static int qcom_battmgr_bat_sm8350_update(struct qcom_battmgr *battmgr, + enum power_supply_property psp) +{ + unsigned int prop; + int ret; + + if (psp >= ARRAY_SIZE(sm8350_bat_prop_map)) + return -EINVAL; + + prop = sm8350_bat_prop_map[psp]; + + mutex_lock(&battmgr->lock); + ret = qcom_battmgr_request_property(battmgr, BATTMGR_BAT_PROPERTY_GET, prop, 0); + mutex_unlock(&battmgr->lock); + + return ret; +} + +static int qcom_battmgr_bat_sc8280xp_update(struct qcom_battmgr *battmgr, + enum power_supply_property psp) +{ + int ret; + + mutex_lock(&battmgr->lock); + + if (!battmgr->info.valid) { + ret = qcom_battmgr_update_info(battmgr); + if (ret < 0) + goto out_unlock; + battmgr->info.valid = true; + } + + ret = qcom_battmgr_update_status(battmgr); + if (ret < 0) + goto out_unlock; + + if (psp == POWER_SUPPLY_PROP_TIME_TO_FULL_AVG) { + ret = qcom_battmgr_update_charge_time(battmgr); + if (ret < 0) { + ret = -ENODATA; + goto out_unlock; + } + } + + if (psp == POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG) { + ret = qcom_battmgr_update_discharge_time(battmgr); + if (ret < 0) { + ret = -ENODATA; + goto out_unlock; + } + } + +out_unlock: + mutex_unlock(&battmgr->lock); + return ret; +} + +static int qcom_battmgr_bat_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct qcom_battmgr *battmgr = power_supply_get_drvdata(psy); + enum qcom_battmgr_unit unit = battmgr->unit; + int ret; + + if (!battmgr->service_up) + return -ENODEV; + + if (battmgr->variant == QCOM_BATTMGR_SC8280XP) + ret = qcom_battmgr_bat_sc8280xp_update(battmgr, psp); + else + ret = qcom_battmgr_bat_sm8350_update(battmgr, psp); + if (ret < 0) + return ret; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = battmgr->status.status; + break; + case POWER_SUPPLY_PROP_CHARGE_TYPE: + val->intval = battmgr->info.charge_type; + break; + case POWER_SUPPLY_PROP_HEALTH: + val->intval = battmgr->status.health; + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = battmgr->info.present; + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = battmgr->info.technology; + break; + case POWER_SUPPLY_PROP_CYCLE_COUNT: + val->intval = battmgr->info.cycle_count; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + val->intval = battmgr->info.voltage_max_design; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX: + val->intval = battmgr->info.voltage_max; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = battmgr->status.voltage_now; + break; + case POWER_SUPPLY_PROP_VOLTAGE_OCV: + val->intval = battmgr->status.voltage_ocv; + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + val->intval = battmgr->status.current_now; + break; + case POWER_SUPPLY_PROP_POWER_NOW: + val->intval = battmgr->status.power_now; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + if (unit != QCOM_BATTMGR_UNIT_mAh) + return -ENODATA; + val->intval = battmgr->info.design_capacity; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL: + if (unit != QCOM_BATTMGR_UNIT_mAh) + return -ENODATA; + val->intval = battmgr->info.last_full_capacity; + break; + case POWER_SUPPLY_PROP_CHARGE_EMPTY: + if (unit != QCOM_BATTMGR_UNIT_mAh) + return -ENODATA; + val->intval = battmgr->info.capacity_low; + break; + case POWER_SUPPLY_PROP_CHARGE_NOW: + if (unit != QCOM_BATTMGR_UNIT_mAh) + return -ENODATA; + val->intval = battmgr->status.capacity; + break; + case POWER_SUPPLY_PROP_CHARGE_COUNTER: + val->intval = battmgr->info.charge_count; + break; + case POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN: + if (unit != QCOM_BATTMGR_UNIT_mWh) + return -ENODATA; + val->intval = battmgr->info.design_capacity; + break; + case POWER_SUPPLY_PROP_ENERGY_FULL: + if (unit != QCOM_BATTMGR_UNIT_mWh) + return -ENODATA; + val->intval = battmgr->info.last_full_capacity; + break; + case POWER_SUPPLY_PROP_ENERGY_EMPTY: + if (unit != QCOM_BATTMGR_UNIT_mWh) + return -ENODATA; + val->intval = battmgr->info.capacity_low; + break; + case POWER_SUPPLY_PROP_ENERGY_NOW: + if (unit != QCOM_BATTMGR_UNIT_mWh) + return -ENODATA; + val->intval = battmgr->status.capacity; + break; + case POWER_SUPPLY_PROP_CAPACITY: + val->intval = battmgr->status.percent; + break; + case POWER_SUPPLY_PROP_TEMP: + val->intval = battmgr->status.temperature; + break; + case POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG: + val->intval = battmgr->status.discharge_time; + break; + case POWER_SUPPLY_PROP_TIME_TO_FULL_AVG: + val->intval = battmgr->status.charge_time; + break; + case POWER_SUPPLY_PROP_MANUFACTURE_YEAR: + val->intval = battmgr->info.year; + break; + case POWER_SUPPLY_PROP_MANUFACTURE_MONTH: + val->intval = battmgr->info.month; + break; + case POWER_SUPPLY_PROP_MANUFACTURE_DAY: + val->intval = battmgr->info.day; + break; + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = battmgr->info.model_number; + break; + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = battmgr->info.oem_info; + break; + case POWER_SUPPLY_PROP_SERIAL_NUMBER: + val->strval = battmgr->info.serial_number; + break; + default: + return -EINVAL; + } + + return 0; +} + +static const enum power_supply_property sc8280xp_bat_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CYCLE_COUNT, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_POWER_NOW, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_CHARGE_EMPTY, + POWER_SUPPLY_PROP_CHARGE_NOW, + POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN, + POWER_SUPPLY_PROP_ENERGY_FULL, + POWER_SUPPLY_PROP_ENERGY_EMPTY, + POWER_SUPPLY_PROP_ENERGY_NOW, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_MANUFACTURE_YEAR, + POWER_SUPPLY_PROP_MANUFACTURE_MONTH, + POWER_SUPPLY_PROP_MANUFACTURE_DAY, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_MANUFACTURER, + POWER_SUPPLY_PROP_SERIAL_NUMBER, +}; + +static const struct power_supply_desc sc8280xp_bat_psy_desc = { + .name = "qcom-battmgr-bat", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = sc8280xp_bat_props, + .num_properties = ARRAY_SIZE(sc8280xp_bat_props), + .get_property = qcom_battmgr_bat_get_property, +}; + +static const enum power_supply_property sm8350_bat_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_CHARGE_TYPE, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_VOLTAGE_OCV, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_MAX, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_TEMP, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CHARGE_COUNTER, + POWER_SUPPLY_PROP_CYCLE_COUNT, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CHARGE_FULL, + POWER_SUPPLY_PROP_MODEL_NAME, + POWER_SUPPLY_PROP_TIME_TO_FULL_AVG, + POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG, + POWER_SUPPLY_PROP_POWER_NOW, +}; + +static const struct power_supply_desc sm8350_bat_psy_desc = { + .name = "qcom-battmgr-bat", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = sm8350_bat_props, + .num_properties = ARRAY_SIZE(sm8350_bat_props), + .get_property = qcom_battmgr_bat_get_property, +}; + +static int qcom_battmgr_ac_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct qcom_battmgr *battmgr = power_supply_get_drvdata(psy); + int ret; + + if (!battmgr->service_up) + return -ENODEV; + + ret = qcom_battmgr_bat_sc8280xp_update(battmgr, psp); + if (ret) + return ret; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = battmgr->ac.online; + break; + default: + return -EINVAL; + } + + return 0; +} + +static const enum power_supply_property sc8280xp_ac_props[] = { + POWER_SUPPLY_PROP_ONLINE, +}; + +static const struct power_supply_desc sc8280xp_ac_psy_desc = { + .name = "qcom-battmgr-ac", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = sc8280xp_ac_props, + .num_properties = ARRAY_SIZE(sc8280xp_ac_props), + .get_property = qcom_battmgr_ac_get_property, +}; + +static const u8 sm8350_usb_prop_map[] = { + [POWER_SUPPLY_PROP_ONLINE] = USB_ONLINE, + [POWER_SUPPLY_PROP_VOLTAGE_NOW] = USB_VOLT_NOW, + [POWER_SUPPLY_PROP_VOLTAGE_MAX] = USB_VOLT_MAX, + [POWER_SUPPLY_PROP_CURRENT_NOW] = USB_CURR_NOW, + [POWER_SUPPLY_PROP_CURRENT_MAX] = USB_CURR_MAX, + [POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT] = USB_INPUT_CURR_LIMIT, + [POWER_SUPPLY_PROP_USB_TYPE] = USB_TYPE, +}; + +static int qcom_battmgr_usb_sm8350_update(struct qcom_battmgr *battmgr, + enum power_supply_property psp) +{ + unsigned int prop; + int ret; + + if (psp >= ARRAY_SIZE(sm8350_usb_prop_map)) + return -EINVAL; + + prop = sm8350_usb_prop_map[psp]; + + mutex_lock(&battmgr->lock); + ret = qcom_battmgr_request_property(battmgr, BATTMGR_USB_PROPERTY_GET, prop, 0); + mutex_unlock(&battmgr->lock); + + return ret; +} + +static int qcom_battmgr_usb_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct qcom_battmgr *battmgr = power_supply_get_drvdata(psy); + int ret; + + if (!battmgr->service_up) + return -ENODEV; + + if (battmgr->variant == QCOM_BATTMGR_SC8280XP) + ret = qcom_battmgr_bat_sc8280xp_update(battmgr, psp); + else + ret = qcom_battmgr_usb_sm8350_update(battmgr, psp); + if (ret) + return ret; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = battmgr->usb.online; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = battmgr->usb.voltage_now; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX: + val->intval = battmgr->usb.voltage_max; + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + val->intval = battmgr->usb.current_now; + break; + case POWER_SUPPLY_PROP_CURRENT_MAX: + val->intval = battmgr->usb.current_max; + break; + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + val->intval = battmgr->usb.current_limit; + break; + case POWER_SUPPLY_PROP_USB_TYPE: + val->intval = battmgr->usb.usb_type; + break; + default: + return -EINVAL; + } + + return 0; +} + +static const enum power_supply_usb_type usb_psy_supported_types[] = { + POWER_SUPPLY_USB_TYPE_UNKNOWN, + POWER_SUPPLY_USB_TYPE_SDP, + POWER_SUPPLY_USB_TYPE_DCP, + POWER_SUPPLY_USB_TYPE_CDP, + POWER_SUPPLY_USB_TYPE_ACA, + POWER_SUPPLY_USB_TYPE_C, + POWER_SUPPLY_USB_TYPE_PD, + POWER_SUPPLY_USB_TYPE_PD_DRP, + POWER_SUPPLY_USB_TYPE_PD_PPS, + POWER_SUPPLY_USB_TYPE_APPLE_BRICK_ID, +}; + +static const enum power_supply_property sc8280xp_usb_props[] = { + POWER_SUPPLY_PROP_ONLINE, +}; + +static const struct power_supply_desc sc8280xp_usb_psy_desc = { + .name = "qcom-battmgr-usb", + .type = POWER_SUPPLY_TYPE_USB, + .properties = sc8280xp_usb_props, + .num_properties = ARRAY_SIZE(sc8280xp_usb_props), + .get_property = qcom_battmgr_usb_get_property, + .usb_types = usb_psy_supported_types, + .num_usb_types = ARRAY_SIZE(usb_psy_supported_types), +}; + +static const enum power_supply_property sm8350_usb_props[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_MAX, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CURRENT_MAX, + POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT, + POWER_SUPPLY_PROP_USB_TYPE, +}; + +static const struct power_supply_desc sm8350_usb_psy_desc = { + .name = "qcom-battmgr-usb", + .type = POWER_SUPPLY_TYPE_USB, + .properties = sm8350_usb_props, + .num_properties = ARRAY_SIZE(sm8350_usb_props), + .get_property = qcom_battmgr_usb_get_property, + .usb_types = usb_psy_supported_types, + .num_usb_types = ARRAY_SIZE(usb_psy_supported_types), +}; + +static const u8 sm8350_wls_prop_map[] = { + [POWER_SUPPLY_PROP_ONLINE] = WLS_ONLINE, + [POWER_SUPPLY_PROP_VOLTAGE_NOW] = WLS_VOLT_NOW, + [POWER_SUPPLY_PROP_VOLTAGE_MAX] = WLS_VOLT_MAX, + [POWER_SUPPLY_PROP_CURRENT_NOW] = WLS_CURR_NOW, + [POWER_SUPPLY_PROP_CURRENT_MAX] = WLS_CURR_MAX, +}; + +static int qcom_battmgr_wls_sm8350_update(struct qcom_battmgr *battmgr, + enum power_supply_property psp) +{ + unsigned int prop; + int ret; + + if (psp >= ARRAY_SIZE(sm8350_wls_prop_map)) + return -EINVAL; + + prop = sm8350_wls_prop_map[psp]; + + mutex_lock(&battmgr->lock); + ret = qcom_battmgr_request_property(battmgr, BATTMGR_WLS_PROPERTY_GET, prop, 0); + mutex_unlock(&battmgr->lock); + + return ret; +} + +static int qcom_battmgr_wls_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct qcom_battmgr *battmgr = power_supply_get_drvdata(psy); + int ret; + + if (!battmgr->service_up) + return -ENODEV; + + if (battmgr->variant == QCOM_BATTMGR_SC8280XP) + ret = qcom_battmgr_bat_sc8280xp_update(battmgr, psp); + else + ret = qcom_battmgr_wls_sm8350_update(battmgr, psp); + if (ret < 0) + return ret; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + val->intval = battmgr->wireless.online; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = battmgr->wireless.voltage_now; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX: + val->intval = battmgr->wireless.voltage_max; + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + val->intval = battmgr->wireless.current_now; + break; + case POWER_SUPPLY_PROP_CURRENT_MAX: + val->intval = battmgr->wireless.current_max; + break; + default: + return -EINVAL; + } + + return 0; +} + +static const enum power_supply_property sc8280xp_wls_props[] = { + POWER_SUPPLY_PROP_ONLINE, +}; + +static const struct power_supply_desc sc8280xp_wls_psy_desc = { + .name = "qcom-battmgr-wls", + .type = POWER_SUPPLY_TYPE_WIRELESS, + .properties = sc8280xp_wls_props, + .num_properties = ARRAY_SIZE(sc8280xp_wls_props), + .get_property = qcom_battmgr_wls_get_property, +}; + +static const enum power_supply_property sm8350_wls_props[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_MAX, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CURRENT_MAX, +}; + +static const struct power_supply_desc sm8350_wls_psy_desc = { + .name = "qcom-battmgr-wls", + .type = POWER_SUPPLY_TYPE_WIRELESS, + .properties = sm8350_wls_props, + .num_properties = ARRAY_SIZE(sm8350_wls_props), + .get_property = qcom_battmgr_wls_get_property, +}; + +static void qcom_battmgr_notification(struct qcom_battmgr *battmgr, + const struct qcom_battmgr_message *msg, + int len) +{ + size_t payload_len = len - sizeof(struct pmic_glink_hdr); + unsigned int notification; + + if (payload_len != sizeof(msg->notification)) { + dev_warn(battmgr->dev, "ignoring notification with invalid length\n"); + return; + } + + notification = le32_to_cpu(msg->notification); + switch (notification) { + case NOTIF_BAT_INFO: + battmgr->info.valid = false; + fallthrough; + case NOTIF_BAT_STATUS: + case NOTIF_BAT_PROPERTY: + power_supply_changed(battmgr->bat_psy); + break; + case NOTIF_USB_PROPERTY: + power_supply_changed(battmgr->usb_psy); + break; + case NOTIF_WLS_PROPERTY: + power_supply_changed(battmgr->wls_psy); + break; + default: + dev_err(battmgr->dev, "unknown notification: %#x\n", notification); + break; + } +} + +static void qcom_battmgr_sc8280xp_strcpy(char *dest, const char *src) +{ + size_t len = src[0]; + + /* Some firmware versions return Pascal-style strings */ + if (len < BATTMGR_STRING_LEN && len == strnlen(src + 1, BATTMGR_STRING_LEN - 1)) { + memcpy(dest, src + 1, len); + dest[len] = '\0'; + } else { + memcpy(dest, src, BATTMGR_STRING_LEN); + } +} + +static unsigned int qcom_battmgr_sc8280xp_parse_technology(const char *chemistry) +{ + if (!strncmp(chemistry, "LIO", BATTMGR_CHEMISTRY_LEN)) + return POWER_SUPPLY_TECHNOLOGY_LION; + + pr_err("Unknown battery technology '%s'\n", chemistry); + return POWER_SUPPLY_TECHNOLOGY_UNKNOWN; +} + +static unsigned int qcom_battmgr_sc8280xp_convert_temp(unsigned int temperature) +{ + return DIV_ROUND_CLOSEST(temperature, 10); +} + +static void qcom_battmgr_sc8280xp_callback(struct qcom_battmgr *battmgr, + const struct qcom_battmgr_message *resp, + size_t len) +{ + unsigned int opcode = le32_to_cpu(resp->hdr.opcode); + unsigned int source; + unsigned int state; + size_t payload_len = len - sizeof(struct pmic_glink_hdr); + + if (payload_len < sizeof(__le32)) { + dev_warn(battmgr->dev, "invalid payload length for %#x: %zd\n", + opcode, len); + return; + } + + switch (opcode) { + case BATTMGR_REQUEST_NOTIFICATION: + battmgr->error = 0; + break; + case BATTMGR_BAT_INFO: + if (payload_len != sizeof(resp->info)) { + dev_warn(battmgr->dev, + "invalid payload length for battery information request: %zd\n", + payload_len); + battmgr->error = -ENODATA; + return; + } + + battmgr->unit = le32_to_cpu(resp->info.power_unit); + + battmgr->info.present = true; + battmgr->info.design_capacity = le32_to_cpu(resp->info.design_capacity) * 1000; + battmgr->info.last_full_capacity = le32_to_cpu(resp->info.last_full_capacity) * 1000; + battmgr->info.voltage_max_design = le32_to_cpu(resp->info.design_voltage) * 1000; + battmgr->info.capacity_low = le32_to_cpu(resp->info.capacity_low) * 1000; + battmgr->info.cycle_count = le32_to_cpu(resp->info.cycle_count); + qcom_battmgr_sc8280xp_strcpy(battmgr->info.model_number, resp->info.model_number); + qcom_battmgr_sc8280xp_strcpy(battmgr->info.serial_number, resp->info.serial_number); + battmgr->info.technology = qcom_battmgr_sc8280xp_parse_technology(resp->info.battery_chemistry); + qcom_battmgr_sc8280xp_strcpy(battmgr->info.oem_info, resp->info.oem_info); + battmgr->info.day = resp->info.day; + battmgr->info.month = resp->info.month; + battmgr->info.year = le16_to_cpu(resp->info.year); + break; + case BATTMGR_BAT_STATUS: + if (payload_len != sizeof(resp->status)) { + dev_warn(battmgr->dev, + "invalid payload length for battery status request: %zd\n", + payload_len); + battmgr->error = -ENODATA; + return; + } + + state = le32_to_cpu(resp->status.battery_state); + if (state & BIT(0)) + battmgr->status.status = POWER_SUPPLY_STATUS_DISCHARGING; + else if (state & BIT(1)) + battmgr->status.status = POWER_SUPPLY_STATUS_CHARGING; + else + battmgr->status.status = POWER_SUPPLY_STATUS_NOT_CHARGING; + + battmgr->status.capacity = le32_to_cpu(resp->status.capacity) * 1000; + battmgr->status.power_now = le32_to_cpu(resp->status.rate) * 1000; + battmgr->status.voltage_now = le32_to_cpu(resp->status.battery_voltage) * 1000; + battmgr->status.temperature = qcom_battmgr_sc8280xp_convert_temp(le32_to_cpu(resp->status.temperature)); + + source = le32_to_cpu(resp->status.charging_source); + battmgr->ac.online = source == BATTMGR_CHARGING_SOURCE_AC; + battmgr->usb.online = source == BATTMGR_CHARGING_SOURCE_USB; + battmgr->wireless.online = source == BATTMGR_CHARGING_SOURCE_WIRELESS; + break; + case BATTMGR_BAT_DISCHARGE_TIME: + battmgr->status.discharge_time = le32_to_cpu(resp->time); + break; + case BATTMGR_BAT_CHARGE_TIME: + battmgr->status.charge_time = le32_to_cpu(resp->time); + break; + default: + dev_warn(battmgr->dev, "unknown message %#x\n", opcode); + break; + } + + complete(&battmgr->ack); +} + +static void qcom_battmgr_sm8350_callback(struct qcom_battmgr *battmgr, + const struct qcom_battmgr_message *resp, + size_t len) +{ + unsigned int property; + unsigned int opcode = le32_to_cpu(resp->hdr.opcode); + size_t payload_len = len - sizeof(struct pmic_glink_hdr); + unsigned int val; + + if (payload_len < sizeof(__le32)) { + dev_warn(battmgr->dev, "invalid payload length for %#x: %zd\n", + opcode, len); + return; + } + + switch (opcode) { + case BATTMGR_BAT_PROPERTY_GET: + property = le32_to_cpu(resp->intval.property); + if (property == BATT_MODEL_NAME) { + if (payload_len != sizeof(resp->strval)) { + dev_warn(battmgr->dev, + "invalid payload length for BATT_MODEL_NAME request: %zd\n", + payload_len); + battmgr->error = -ENODATA; + return; + } + } else { + if (payload_len != sizeof(resp->intval)) { + dev_warn(battmgr->dev, + "invalid payload length for %#x request: %zd\n", + property, payload_len); + battmgr->error = -ENODATA; + return; + } + + battmgr->error = le32_to_cpu(resp->intval.result); + if (battmgr->error) + goto out_complete; + } + + switch (property) { + case BATT_STATUS: + battmgr->status.status = le32_to_cpu(resp->intval.value); + break; + case BATT_HEALTH: + battmgr->status.health = le32_to_cpu(resp->intval.value); + break; + case BATT_PRESENT: + battmgr->info.present = le32_to_cpu(resp->intval.value); + break; + case BATT_CHG_TYPE: + battmgr->info.charge_type = le32_to_cpu(resp->intval.value); + break; + case BATT_CAPACITY: + battmgr->status.percent = le32_to_cpu(resp->intval.value); + do_div(battmgr->status.percent, 100); + break; + case BATT_VOLT_OCV: + battmgr->status.voltage_ocv = le32_to_cpu(resp->intval.value); + break; + case BATT_VOLT_NOW: + battmgr->status.voltage_now = le32_to_cpu(resp->intval.value); + break; + case BATT_VOLT_MAX: + battmgr->info.voltage_max = le32_to_cpu(resp->intval.value); + break; + case BATT_CURR_NOW: + battmgr->status.current_now = le32_to_cpu(resp->intval.value); + break; + case BATT_TEMP: + val = le32_to_cpu(resp->intval.value); + battmgr->status.temperature = DIV_ROUND_CLOSEST(val, 10); + break; + case BATT_TECHNOLOGY: + battmgr->info.technology = le32_to_cpu(resp->intval.value); + break; + case BATT_CHG_COUNTER: + battmgr->info.charge_count = le32_to_cpu(resp->intval.value); + break; + case BATT_CYCLE_COUNT: + battmgr->info.cycle_count = le32_to_cpu(resp->intval.value); + break; + case BATT_CHG_FULL_DESIGN: + battmgr->info.design_capacity = le32_to_cpu(resp->intval.value); + break; + case BATT_CHG_FULL: + battmgr->info.last_full_capacity = le32_to_cpu(resp->intval.value); + break; + case BATT_MODEL_NAME: + strscpy(battmgr->info.model_number, resp->strval.model, BATTMGR_STRING_LEN); + break; + case BATT_TTF_AVG: + battmgr->status.charge_time = le32_to_cpu(resp->intval.value); + break; + case BATT_TTE_AVG: + battmgr->status.discharge_time = le32_to_cpu(resp->intval.value); + break; + case BATT_POWER_NOW: + battmgr->status.power_now = le32_to_cpu(resp->intval.value); + break; + default: + dev_warn(battmgr->dev, "unknown property %#x\n", property); + break; + } + break; + case BATTMGR_USB_PROPERTY_GET: + property = le32_to_cpu(resp->intval.property); + if (payload_len != sizeof(resp->intval)) { + dev_warn(battmgr->dev, + "invalid payload length for %#x request: %zd\n", + property, payload_len); + battmgr->error = -ENODATA; + return; + } + + battmgr->error = le32_to_cpu(resp->intval.result); + if (battmgr->error) + goto out_complete; + + switch (property) { + case USB_ONLINE: + battmgr->usb.online = le32_to_cpu(resp->intval.value); + break; + case USB_VOLT_NOW: + battmgr->usb.voltage_now = le32_to_cpu(resp->intval.value); + break; + case USB_VOLT_MAX: + battmgr->usb.voltage_max = le32_to_cpu(resp->intval.value); + break; + case USB_CURR_NOW: + battmgr->usb.current_now = le32_to_cpu(resp->intval.value); + break; + case USB_CURR_MAX: + battmgr->usb.current_max = le32_to_cpu(resp->intval.value); + break; + case USB_INPUT_CURR_LIMIT: + battmgr->usb.current_limit = le32_to_cpu(resp->intval.value); + break; + case USB_TYPE: + battmgr->usb.usb_type = le32_to_cpu(resp->intval.value); + break; + default: + dev_warn(battmgr->dev, "unknown property %#x\n", property); + break; + } + break; + case BATTMGR_WLS_PROPERTY_GET: + property = le32_to_cpu(resp->intval.property); + if (payload_len != sizeof(resp->intval)) { + dev_warn(battmgr->dev, + "invalid payload length for %#x request: %zd\n", + property, payload_len); + battmgr->error = -ENODATA; + return; + } + + battmgr->error = le32_to_cpu(resp->intval.result); + if (battmgr->error) + goto out_complete; + + switch (property) { + case WLS_ONLINE: + battmgr->wireless.online = le32_to_cpu(resp->intval.value); + break; + case WLS_VOLT_NOW: + battmgr->wireless.voltage_now = le32_to_cpu(resp->intval.value); + break; + case WLS_VOLT_MAX: + battmgr->wireless.voltage_max = le32_to_cpu(resp->intval.value); + break; + case WLS_CURR_NOW: + battmgr->wireless.current_now = le32_to_cpu(resp->intval.value); + break; + case WLS_CURR_MAX: + battmgr->wireless.current_max = le32_to_cpu(resp->intval.value); + break; + default: + dev_warn(battmgr->dev, "unknown property %#x\n", property); + break; + } + break; + case BATTMGR_REQUEST_NOTIFICATION: + battmgr->error = 0; + break; + default: + dev_warn(battmgr->dev, "unknown message %#x\n", opcode); + break; + } + +out_complete: + complete(&battmgr->ack); +} + +static void qcom_battmgr_callback(const void *data, size_t len, void *priv) +{ + const struct pmic_glink_hdr *hdr = data; + struct qcom_battmgr *battmgr = priv; + unsigned int opcode = le32_to_cpu(hdr->opcode); + + if (opcode == BATTMGR_NOTIFICATION) + qcom_battmgr_notification(battmgr, data, len); + else if (battmgr->variant == QCOM_BATTMGR_SC8280XP) + qcom_battmgr_sc8280xp_callback(battmgr, data, len); + else + qcom_battmgr_sm8350_callback(battmgr, data, len); +} + +static void qcom_battmgr_enable_worker(struct work_struct *work) +{ + struct qcom_battmgr *battmgr = container_of(work, struct qcom_battmgr, enable_work); + struct qcom_battmgr_enable_request req = { + .hdr.owner = PMIC_GLINK_OWNER_BATTMGR, + .hdr.type = PMIC_GLINK_NOTIFY, + .hdr.opcode = BATTMGR_REQUEST_NOTIFICATION, + }; + int ret; + + ret = qcom_battmgr_request(battmgr, &req, sizeof(req)); + if (ret) + dev_err(battmgr->dev, "failed to request power notifications\n"); +} + +static void qcom_battmgr_pdr_notify(void *priv, int state) +{ + struct qcom_battmgr *battmgr = priv; + + if (state == SERVREG_SERVICE_STATE_UP) { + battmgr->service_up = true; + schedule_work(&battmgr->enable_work); + } else { + battmgr->service_up = false; + } +} + +static const struct of_device_id qcom_battmgr_of_variants[] = { + { .compatible = "qcom,sc8180x-pmic-glink", .data = (void *)QCOM_BATTMGR_SC8280XP }, + { .compatible = "qcom,sc8280xp-pmic-glink", .data = (void *)QCOM_BATTMGR_SC8280XP }, + /* Unmatched devices falls back to QCOM_BATTMGR_SM8350 */ + {} +}; + +static char *qcom_battmgr_battery[] = { "battery" }; + +static int qcom_battmgr_probe(struct auxiliary_device *adev, + const struct auxiliary_device_id *id) +{ + struct power_supply_config psy_cfg_supply = {}; + struct power_supply_config psy_cfg = {}; + const struct of_device_id *match; + struct qcom_battmgr *battmgr; + struct device *dev = &adev->dev; + + battmgr = devm_kzalloc(dev, sizeof(*battmgr), GFP_KERNEL); + if (!battmgr) + return -ENOMEM; + + battmgr->dev = dev; + + psy_cfg.drv_data = battmgr; + psy_cfg.of_node = adev->dev.of_node; + + psy_cfg_supply.drv_data = battmgr; + psy_cfg_supply.of_node = adev->dev.of_node; + psy_cfg_supply.supplied_to = qcom_battmgr_battery; + psy_cfg_supply.num_supplicants = 1; + + INIT_WORK(&battmgr->enable_work, qcom_battmgr_enable_worker); + mutex_init(&battmgr->lock); + init_completion(&battmgr->ack); + + match = of_match_device(qcom_battmgr_of_variants, dev->parent); + if (match) + battmgr->variant = (unsigned long)match->data; + else + battmgr->variant = QCOM_BATTMGR_SM8350; + + if (battmgr->variant == QCOM_BATTMGR_SC8280XP) { + battmgr->bat_psy = devm_power_supply_register(dev, &sc8280xp_bat_psy_desc, &psy_cfg); + if (IS_ERR(battmgr->bat_psy)) + return dev_err_probe(dev, PTR_ERR(battmgr->bat_psy), + "failed to register battery power supply\n"); + + battmgr->ac_psy = devm_power_supply_register(dev, &sc8280xp_ac_psy_desc, &psy_cfg_supply); + if (IS_ERR(battmgr->ac_psy)) + return dev_err_probe(dev, PTR_ERR(battmgr->ac_psy), + "failed to register AC power supply\n"); + + battmgr->usb_psy = devm_power_supply_register(dev, &sc8280xp_usb_psy_desc, &psy_cfg_supply); + if (IS_ERR(battmgr->usb_psy)) + return dev_err_probe(dev, PTR_ERR(battmgr->usb_psy), + "failed to register USB power supply\n"); + + battmgr->wls_psy = devm_power_supply_register(dev, &sc8280xp_wls_psy_desc, &psy_cfg_supply); + if (IS_ERR(battmgr->wls_psy)) + return dev_err_probe(dev, PTR_ERR(battmgr->wls_psy), + "failed to register wireless charing power supply\n"); + } else { + battmgr->bat_psy = devm_power_supply_register(dev, &sm8350_bat_psy_desc, &psy_cfg); + if (IS_ERR(battmgr->bat_psy)) + return dev_err_probe(dev, PTR_ERR(battmgr->bat_psy), + "failed to register battery power supply\n"); + + battmgr->usb_psy = devm_power_supply_register(dev, &sm8350_usb_psy_desc, &psy_cfg_supply); + if (IS_ERR(battmgr->usb_psy)) + return dev_err_probe(dev, PTR_ERR(battmgr->usb_psy), + "failed to register USB power supply\n"); + + battmgr->wls_psy = devm_power_supply_register(dev, &sm8350_wls_psy_desc, &psy_cfg_supply); + if (IS_ERR(battmgr->wls_psy)) + return dev_err_probe(dev, PTR_ERR(battmgr->wls_psy), + "failed to register wireless charing power supply\n"); + } + + battmgr->client = devm_pmic_glink_register_client(dev, + PMIC_GLINK_OWNER_BATTMGR, + qcom_battmgr_callback, + qcom_battmgr_pdr_notify, + battmgr); + return PTR_ERR_OR_ZERO(battmgr->client); +} + +static const struct auxiliary_device_id qcom_battmgr_id_table[] = { + { .name = "pmic_glink.power-supply", }, + {}, +}; +MODULE_DEVICE_TABLE(auxiliary, qcom_battmgr_id_table); + +static struct auxiliary_driver qcom_battmgr_driver = { + .name = "pmic_glink_power_supply", + .probe = qcom_battmgr_probe, + .id_table = qcom_battmgr_id_table, +}; + +static int __init qcom_battmgr_init(void) +{ + return auxiliary_driver_register(&qcom_battmgr_driver); +} +module_init(qcom_battmgr_init); + +static void __exit qcom_battmgr_exit(void) +{ + auxiliary_driver_unregister(&qcom_battmgr_driver); +} +module_exit(qcom_battmgr_exit); + +MODULE_DESCRIPTION("Qualcomm PMIC GLINK battery manager driver"); +MODULE_LICENSE("GPL");