[PATCH 14/22] platform/chrome: cros_typec_switch: Add support for signaling HPD to drm_bridge

From: Stephen Boyd
Date: Sat Feb 10 2024 - 02:14:54 EST


We can imagine that logically the EC is a device that has some number of
DisplayPort (DP) connector inputs, some number of USB3 connector inputs,
and some number of USB type-c connector outputs. If you squint enough it
looks like a USB type-c dock. Logically there's a crossbar pin
assignment capability within the EC that can assign USB and DP lanes to
USB type-c lanes in the connector (i.e. USB type-c pin configurations).
In reality, the EC is a microcontroller that has some TCPCs and
redrivers connected to it over something like i2c and DP/USB from the AP
is wired directly to those ICs, not the EC.

This design allows the EC to abstract many possible USB and DP hardware
configurations away from the AP (kernel) so that the AP can largely deal
with USB and DP without thinking about USB Type-C much at all. The DP
and USB data originate in the AP, not the EC, so it helps to think that
the EC takes the DP and USB data as input to mux onto USB type-c ports
even if it really doesn't do that. With this split design, the EC
forwards the DP HPD state to the AP via a GPIO that's connected to the
DP phy.

Having that HPD state signaled directly to the DP phy uses precious
hardware resources, a GPIO or two and a wire, and it also forces the
TCPM to live on the EC. If we want to save costs and move more control
of USB type-c to the kernel it's in our interest to get rid of the HPD
GPIO entirely and signal HPD to the DP phy some other way. Luckily, the
EC already exposes information about the USB Type-C stack to the kernel
via the host command interface in the "google,cros-ec-typec" compatible
driver, which parses EC messages related to USB type-c and effectively
"replays" those messages to the kernel's USB typec subsystem. This
includes the state of HPD, which can be interrogated and acted upon by
registering a 'struct typec_mux_dev' with the typec subsystem.

On DT based systems, the DP display pipeline is abstracted via a 'struct
drm_bridge'. If we want to signal HPD state within the kernel we need to
hook into the drm_bridge framework somehow to call
drm_bridge_hpd_notify() when HPD state changes in the typec framework.
Make a drm_bridge in the EC that attaches onto the end of the DP bridge
chain and logically moves the display data onto a usb-c-connector.
Signal HPD when the typec HPD state changes, as long as this new
drm_bridge is the one that's supposed to signal HPD. Do that by
registering a 'struct typec_mux_dev' with the typec framework and
associating that struct with a usb-c-connector node and a drm_bridge.

To keep this patch minimal, just signal HPD state to the drm_bridge
chain. Later patches will add more features. Eventually we'll be able to
inform userspace about which usb-c-connector node is displaying DP and
what USB devices are connected to a connector. Note that this code is
placed in the cros_typec_switch driver because that's where mode-switch
devices on the EC are controlled by the AP. Logically this drm_bridge
sits in front of the mode-switch on the EC, and if there is anything to
control on the EC the 'EC_FEATURE_TYPEC_AP_MUX_SET' feature will be set.

Cc: Prashant Malani <pmalani@xxxxxxxxxxxx>
Cc: Benson Leung <bleung@xxxxxxxxxxxx>
Cc: Tzung-Bi Shih <tzungbi@xxxxxxxxxx>
Cc: <chrome-platform@xxxxxxxxxxxxxxx>
Cc: Pin-yen Lin <treapking@xxxxxxxxxxxx>
Signed-off-by: Stephen Boyd <swboyd@xxxxxxxxxxxx>
---
drivers/platform/chrome/Kconfig | 3 +-
drivers/platform/chrome/cros_typec_switch.c | 218 ++++++++++++++++++--
2 files changed, 204 insertions(+), 17 deletions(-)

diff --git a/drivers/platform/chrome/Kconfig b/drivers/platform/chrome/Kconfig
index 7a83346bfa53..910aa8be9c84 100644
--- a/drivers/platform/chrome/Kconfig
+++ b/drivers/platform/chrome/Kconfig
@@ -287,7 +287,8 @@ config CHROMEOS_PRIVACY_SCREEN

config CROS_TYPEC_SWITCH
tristate "ChromeOS EC Type-C Switch Control"
- depends on MFD_CROS_EC_DEV && TYPEC && ACPI
+ depends on MFD_CROS_EC_DEV
+ depends on TYPEC
default MFD_CROS_EC_DEV
help
If you say Y here, you get support for configuring the ChromeOS EC Type-C
diff --git a/drivers/platform/chrome/cros_typec_switch.c b/drivers/platform/chrome/cros_typec_switch.c
index 769de2889f2f..d8fb6662cf8d 100644
--- a/drivers/platform/chrome/cros_typec_switch.c
+++ b/drivers/platform/chrome/cros_typec_switch.c
@@ -10,6 +10,7 @@
#include <linux/delay.h>
#include <linux/iopoll.h>
#include <linux/module.h>
+#include <linux/of.h>
#include <linux/platform_data/cros_ec_commands.h>
#include <linux/platform_data/cros_ec_proto.h>
#include <linux/platform_device.h>
@@ -18,6 +19,15 @@
#include <linux/usb/typec_mux.h>
#include <linux/usb/typec_retimer.h>

+#include <drm/drm_bridge.h>
+#include <drm/drm_print.h>
+
+struct cros_typec_dp_bridge {
+ struct cros_typec_switch_data *sdata;
+ bool hpd_enabled;
+ struct drm_bridge bridge;
+};
+
/* Handles and other relevant data required for each port's switches. */
struct cros_typec_port {
int port_num;
@@ -30,7 +40,9 @@ struct cros_typec_port {
struct cros_typec_switch_data {
struct device *dev;
struct cros_ec_device *ec;
+ bool typec_cmd_supported;
struct cros_typec_port *ports[EC_USB_PD_MAX_PORTS];
+ struct cros_typec_dp_bridge *typec_dp_bridge;
};

static int cros_typec_cmd_mux_set(struct cros_typec_switch_data *sdata, int port_num, u8 index,
@@ -143,13 +155,60 @@ static int cros_typec_configure_mux(struct cros_typec_switch_data *sdata, int po
return 0;
}

+static int cros_typec_dp_port_switch_set(struct typec_mux_dev *mode_switch,
+ struct typec_mux_state *state)
+{
+ struct cros_typec_port *port;
+ const struct typec_displayport_data *dp_data;
+ struct cros_typec_dp_bridge *typec_dp_bridge;
+ struct drm_bridge *bridge;
+ bool hpd_asserted;
+
+ port = typec_mux_get_drvdata(mode_switch);
+ typec_dp_bridge = port->sdata->typec_dp_bridge;
+ if (!typec_dp_bridge)
+ return 0;
+
+ bridge = &typec_dp_bridge->bridge;
+
+ if (state->mode == TYPEC_STATE_SAFE || state->mode == TYPEC_STATE_USB) {
+ if (typec_dp_bridge->hpd_enabled)
+ drm_bridge_hpd_notify(bridge, connector_status_disconnected);
+
+ return 0;
+ }
+
+ if (state->alt && state->alt->svid == USB_TYPEC_DP_SID) {
+ if (typec_dp_bridge->hpd_enabled) {
+ dp_data = state->data;
+ hpd_asserted = dp_data->status & DP_STATUS_HPD_STATE;
+
+ if (hpd_asserted)
+ drm_bridge_hpd_notify(bridge, connector_status_connected);
+ else
+ drm_bridge_hpd_notify(bridge, connector_status_disconnected);
+ }
+ }
+
+ return 0;
+}
+
static int cros_typec_mode_switch_set(struct typec_mux_dev *mode_switch,
struct typec_mux_state *state)
{
struct cros_typec_port *port = typec_mux_get_drvdata(mode_switch);
+ struct cros_typec_switch_data *sdata = port->sdata;
+ int ret;
+
+ ret = cros_typec_dp_port_switch_set(mode_switch, state);
+ if (ret)
+ return ret;

/* Mode switches have index 0. */
- return cros_typec_configure_mux(port->sdata, port->port_num, 0, state->mode, state->alt);
+ if (sdata->typec_cmd_supported)
+ return cros_typec_configure_mux(port->sdata, port->port_num, 0, state->mode, state->alt);
+
+ return 0;
}

static int cros_typec_retimer_set(struct typec_retimer *retimer, struct typec_retimer_state *state)
@@ -201,12 +260,77 @@ static int cros_typec_register_retimer(struct cros_typec_port *port, struct fwno
return PTR_ERR_OR_ZERO(port->retimer);
}

+static int
+cros_typec_dp_bridge_attach(struct drm_bridge *bridge,
+ enum drm_bridge_attach_flags flags)
+{
+ if (!(flags & DRM_BRIDGE_ATTACH_NO_CONNECTOR)) {
+ DRM_ERROR("Fix bridge driver to make connector optional!\n");
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static struct cros_typec_dp_bridge *
+bridge_to_cros_typec_dp_bridge(struct drm_bridge *bridge)
+{
+ return container_of(bridge, struct cros_typec_dp_bridge, bridge);
+}
+
+static void cros_typec_dp_bridge_hpd_enable(struct drm_bridge *bridge)
+{
+ struct cros_typec_dp_bridge *typec_dp_bridge;
+
+ typec_dp_bridge = bridge_to_cros_typec_dp_bridge(bridge);
+ typec_dp_bridge->hpd_enabled = true;
+}
+
+static void cros_typec_dp_bridge_hpd_disable(struct drm_bridge *bridge)
+{
+ struct cros_typec_dp_bridge *typec_dp_bridge;
+
+ typec_dp_bridge = bridge_to_cros_typec_dp_bridge(bridge);
+ typec_dp_bridge->hpd_enabled = false;
+}
+
+static const struct drm_bridge_funcs cros_typec_dp_bridge_funcs = {
+ .attach = cros_typec_dp_bridge_attach,
+ .hpd_enable = cros_typec_dp_bridge_hpd_enable,
+ .hpd_disable = cros_typec_dp_bridge_hpd_disable,
+};
+
+static int cros_typec_register_dp_bridge(struct cros_typec_switch_data *sdata,
+ struct fwnode_handle *fwnode)
+{
+ struct cros_typec_dp_bridge *typec_dp_bridge;
+ struct drm_bridge *bridge;
+ struct device *dev = sdata->dev;
+
+ typec_dp_bridge = devm_kzalloc(dev, sizeof(*typec_dp_bridge), GFP_KERNEL);
+ if (!typec_dp_bridge)
+ return -ENOMEM;
+
+ typec_dp_bridge->sdata = sdata;
+ sdata->typec_dp_bridge = typec_dp_bridge;
+ bridge = &typec_dp_bridge->bridge;
+
+ bridge->funcs = &cros_typec_dp_bridge_funcs;
+ bridge->of_node = dev->of_node;
+ bridge->type = DRM_MODE_CONNECTOR_DisplayPort;
+ bridge->ops |= DRM_BRIDGE_OP_HPD;
+
+ return devm_drm_bridge_add(dev, bridge);
+}
+
static int cros_typec_register_port(struct cros_typec_switch_data *sdata,
struct fwnode_handle *fwnode)
{
struct cros_typec_port *port;
struct device *dev = sdata->dev;
struct acpi_device *adev;
+ struct device_node *np;
+ struct fwnode_handle *port_node;
u32 index;
int ret;
const char *prop_name;
@@ -218,9 +342,12 @@ static int cros_typec_register_port(struct cros_typec_switch_data *sdata,
adev = to_acpi_device_node(fwnode);
if (adev)
prop_name = "_ADR";
+ np = to_of_node(fwnode);
+ if (np)
+ prop_name = "reg";

- if (!adev)
- return dev_err_probe(fwnode->dev, -ENODEV, "Couldn't get ACPI handle\n");
+ if (!adev && !np)
+ return dev_err_probe(fwnode->dev, -ENODEV, "Couldn't get ACPI/OF device handle\n");

ret = fwnode_property_read_u32(fwnode, prop_name, &index);
if (ret)
@@ -232,41 +359,84 @@ static int cros_typec_register_port(struct cros_typec_switch_data *sdata,
port->port_num = index;
sdata->ports[index] = port;

+ port_node = fwnode;
+ if (np)
+ fwnode = fwnode_graph_get_port_parent(fwnode);
+
if (fwnode_property_present(fwnode, "retimer-switch")) {
- ret = cros_typec_register_retimer(port, fwnode);
- if (ret)
- return dev_err_probe(dev, ret, "Retimer switch register failed\n");
+ ret = cros_typec_register_retimer(port, port_node);
+ if (ret) {
+ dev_err_probe(dev, ret, "Retimer switch register failed\n");
+ goto out;
+ }

dev_dbg(dev, "Retimer switch registered for index %u\n", index);
}

- if (!fwnode_property_present(fwnode, "mode-switch"))
- return 0;
+ if (fwnode_property_present(fwnode, "mode-switch")) {
+ ret = cros_typec_register_mode_switch(port, port_node);
+ if (ret) {
+ dev_err_probe(dev, ret, "Mode switch register failed\n");
+ goto out;
+ }

- ret = cros_typec_register_mode_switch(port, fwnode);
- if (ret)
- return dev_err_probe(dev, ret, "Mode switch register failed\n");
+ dev_dbg(dev, "Mode switch registered for index %u\n", index);
+ }

- dev_dbg(dev, "Mode switch registered for index %u\n", index);

+out:
+ if (np)
+ fwnode_handle_put(fwnode);
return ret;
}

static int cros_typec_register_switches(struct cros_typec_switch_data *sdata)
{
struct device *dev = sdata->dev;
+ struct fwnode_handle *devnode;
struct fwnode_handle *fwnode;
+ struct fwnode_endpoint endpoint;
int nports, ret;

nports = device_get_child_node_count(dev);
if (nports == 0)
return dev_err_probe(dev, -ENODEV, "No switch devices found\n");

- device_for_each_child_node(dev, fwnode) {
- ret = cros_typec_register_port(sdata, fwnode);
- if (ret) {
+ devnode = dev_fwnode(dev);
+ if (fwnode_graph_get_endpoint_count(devnode, 0)) {
+ fwnode_graph_for_each_endpoint(devnode, fwnode) {
+ ret = fwnode_graph_parse_endpoint(fwnode, &endpoint);
+ if (ret) {
+ fwnode_handle_put(fwnode);
+ goto err;
+ }
+ /* Skip if not a type-c output port */
+ if (endpoint.port != 2)
+ continue;
+
+ ret = cros_typec_register_port(sdata, fwnode);
+ if (ret) {
+ fwnode_handle_put(fwnode);
+ goto err;
+ }
+ }
+ } else {
+ device_for_each_child_node(dev, fwnode) {
+ ret = cros_typec_register_port(sdata, fwnode);
+ if (ret) {
+ fwnode_handle_put(fwnode);
+ goto err;
+ }
+ }
+ }
+
+ if (fwnode_property_present(devnode, "mode-switch")) {
+ fwnode = fwnode_graph_get_endpoint_by_id(devnode, 0, 0, 0);
+ if (fwnode) {
+ ret = cros_typec_register_dp_bridge(sdata, fwnode);
fwnode_handle_put(fwnode);
- goto err;
+ if (ret)
+ goto err;
}
}

@@ -280,6 +450,7 @@ static int cros_typec_switch_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct cros_typec_switch_data *sdata;
+ struct cros_ec_dev *ec_dev;

sdata = devm_kzalloc(dev, sizeof(*sdata), GFP_KERNEL);
if (!sdata)
@@ -288,6 +459,12 @@ static int cros_typec_switch_probe(struct platform_device *pdev)
sdata->dev = dev;
sdata->ec = dev_get_drvdata(pdev->dev.parent);

+ ec_dev = dev_get_drvdata(&sdata->ec->ec->dev);
+ if (!ec_dev)
+ return -EPROBE_DEFER;
+
+ sdata->typec_cmd_supported = cros_ec_check_features(ec_dev, EC_FEATURE_TYPEC_AP_MUX_SET);
+
platform_set_drvdata(pdev, sdata);

return cros_typec_register_switches(sdata);
@@ -308,10 +485,19 @@ static const struct acpi_device_id cros_typec_switch_acpi_id[] = {
MODULE_DEVICE_TABLE(acpi, cros_typec_switch_acpi_id);
#endif

+#ifdef CONFIG_OF
+static const struct of_device_id cros_typec_switch_of_match_table[] = {
+ { .compatible = "google,cros-ec-typec-switch" },
+ {}
+};
+MODULE_DEVICE_TABLE(of, cros_typec_switch_of_match_table);
+#endif
+
static struct platform_driver cros_typec_switch_driver = {
.driver = {
.name = "cros-typec-switch",
.acpi_match_table = ACPI_PTR(cros_typec_switch_acpi_id),
+ .of_match_table = of_match_ptr(cros_typec_switch_of_match_table),
},
.probe = cros_typec_switch_probe,
.remove_new = cros_typec_switch_remove,
--
https://chromeos.dev