* [RFC PATCH 1/4] soc: qcom: pmic_glink: Hold back notifications over system sleep
2026-10-22 16:41 [RFC PATCH 0/4] soc: qcom: pmic_glink: Hold back notifications over system sleep Vladyslav Tymoshchuk
@ 2026-10-22 16:41 ` Vladyslav Tymoshchuk
2026-10-22 16:41 ` [RFC PATCH 2/4] soc: qcom: pmic_glink: Report wakeup events on behalf of clients Vladyslav Tymoshchuk
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Vladyslav Tymoshchuk @ 2026-10-22 16:41 UTC (permalink / raw)
To: Bjorn Andersson, Konrad Dybcio
Cc: Abel Vesa, Heikki Krogerus, Greg Kroah-Hartman,
Sebastian Reichel, Krzysztof Kozlowski, Dmitry Baryshkov,
Casey Connolly, Johan Hovold, Jishnu Prakash, Kamal Wadhwa,
Joonhoe Kim, Daniel J Blueman, Neil Armstrong, Kees Cook,
Gustavo A . R . Silva, linux-arm-msm, linux-usb, linux-pm,
linux-hardening, linux-kernel, Vladyslav Tymoshchuk
The GLINK interrupt is not suspended, so the PMIC GLINK messages keep
reaching the clients during system suspend and resume, and while the
system is in suspend-to-idle. The clients handle most notifications in
work on the system workqueue, which is not freezable, so the work can
run before the devices it uses have resumed:
- ucsi_glink passes connector changes on to the UCSI core, whose
connector work switches the USB role and sets the Type-C muxes and
switches. On SM8750, unplugging the USB cable while the system is in
suspend-to-idle makes dwc3-qcom access its registers with the clocks
off, and the SoC resets. Other boards hit I2C transfers on suspended
controllers from the same work, to a PS8830 retimer or an FSA4480
switch.
- pmic_glink_altmode had the same problem with its worker, which
commit c7c56b6ec077 ("soc: qcom: pmic_glink: Fix device access from
worker during suspend") moved to system_freezable_wq as a stop-gap.
Rather than have each client defer its work, defer the notifications
in pmic_glink: queue them from ->prepare() on, and from ->complete()
schedule work on system_freezable_wq that passes them on to the
clients. The work runs once the tasks have been thawed, after all the
devices have resumed. Until it has run, new notifications are queued
behind the old ones, so the clients get them in order. Responses are
passed on right away, as the client that sent the request is waiting
for them.
The clients treat a notification as the latest state, or as a hint to
read the current state, so an identical notification adds nothing:
keep only the latest copy. Keep at most 64 notifications, drop the
oldest beyond that, and say how many were lost.
Hold back the service UP too, as on UP the clients start using the
remote, and pass it on after the deferred notifications. Pass DOWN on
right away, so that the clients stop using the remote, and drop the
deferred notifications with it, as they came from the remote that went
away.
ucsi_glink learns from a notification that a UCSI command has
completed, so a command still in flight at ->prepare() completes only
after the system has resumed. The timeout of the UCSI core does not
count the time spent in suspend, only the time the system takes to
suspend and resume.
qcom_battmgr calls power_supply_changed() for most notifications, which
keeps the system awake through the wakeup source of the power supply.
For a notification that is held back, this now happens after the
system has resumed, so it no longer aborts a suspend started through
/sys/power/wakeup_count.
Fixes: 58ef4ece1e41 ("soc: qcom: pmic_glink: Introduce base PMIC GLINK driver")
Signed-off-by: Vladyslav Tymoshchuk <tvlad744@gmail.com>
---
I used an AI assistant (Claude) for the analysis and to draft this
patch and its description; I reviewed the result. The testing is
described in the cover letter.
drivers/soc/qcom/pmic_glink.c | 177 ++++++++++++++++++++++++++++++++--
1 file changed, 168 insertions(+), 9 deletions(-)
diff --git a/drivers/soc/qcom/pmic_glink.c b/drivers/soc/qcom/pmic_glink.c
index 84fd0c82c0c4..748b335be6ef 100644
--- a/drivers/soc/qcom/pmic_glink.c
+++ b/drivers/soc/qcom/pmic_glink.c
@@ -14,8 +14,11 @@
#include <linux/soc/qcom/pdr.h>
#include <linux/soc/qcom/pmic_glink.h>
#include <linux/spinlock.h>
+#include <linux/string.h>
+#include <linux/workqueue.h>
#define PMIC_GLINK_SEND_TIMEOUT (5 * HZ)
+#define PMIC_GLINK_MAX_DEFERRED 64
enum {
PMIC_GLINK_CLIENT_BATT = 0,
@@ -50,6 +53,14 @@ struct pmic_glink {
/* serializing clients list updates */
spinlock_t client_lock;
struct list_head clients;
+
+ /* notifications held back over system sleep */
+ spinlock_t defer_lock;
+ bool defer;
+ struct list_head deferred;
+ unsigned int num_deferred;
+ unsigned int num_dropped;
+ struct work_struct deliver_work;
};
static struct pmic_glink *__pmic_glink;
@@ -66,6 +77,12 @@ struct pmic_glink_client {
void *priv;
};
+struct pmic_glink_deferred_msg {
+ struct list_head node;
+ size_t len;
+ u8 data[] __counted_by(len);
+};
+
static void _devm_pmic_glink_release_client(struct device *dev, void *res)
{
struct pmic_glink_client *client = (struct pmic_glink_client *)res;
@@ -149,13 +166,77 @@ int pmic_glink_send(struct pmic_glink_client *client, void *data, size_t len)
}
EXPORT_SYMBOL_GPL(pmic_glink_send);
+static void pmic_glink_dispatch(struct pmic_glink *pg, const void *data, size_t len)
+{
+ const struct pmic_glink_hdr *hdr = data;
+ struct pmic_glink_client *client;
+ unsigned long flags;
+
+ spin_lock_irqsave(&pg->client_lock, flags);
+ list_for_each_entry(client, &pg->clients, node) {
+ if (client->id == le32_to_cpu(hdr->owner))
+ client->cb(data, len, client->priv);
+ }
+ spin_unlock_irqrestore(&pg->client_lock, flags);
+}
+
+static bool pmic_glink_defer(struct pmic_glink *pg, const void *data, size_t len)
+{
+ struct pmic_glink_deferred_msg *msg;
+
+ guard(spinlock_irqsave)(&pg->defer_lock);
+
+ if (!pg->defer)
+ return false;
+
+ /* An identical notification brings nothing new, keep the latest copy */
+ list_for_each_entry(msg, &pg->deferred, node) {
+ if (msg->len == len && !memcmp(msg->data, data, len)) {
+ list_move_tail(&msg->node, &pg->deferred);
+ return true;
+ }
+ }
+
+ if (pg->num_deferred == PMIC_GLINK_MAX_DEFERRED) {
+ msg = list_first_entry(&pg->deferred, struct pmic_glink_deferred_msg, node);
+ list_del(&msg->node);
+ kfree(msg);
+ pg->num_deferred--;
+ pg->num_dropped++;
+ }
+
+ msg = kmalloc_flex(*msg, data, len, GFP_ATOMIC);
+ if (!msg) {
+ pg->num_dropped++;
+ return true;
+ }
+
+ msg->len = len;
+ memcpy(msg->data, data, len);
+ list_add_tail(&msg->node, &pg->deferred);
+ pg->num_deferred++;
+
+ return true;
+}
+
+static void pmic_glink_drop_deferred(struct pmic_glink *pg)
+{
+ struct pmic_glink_deferred_msg *msg, *tmp;
+
+ guard(spinlock_irqsave)(&pg->defer_lock);
+
+ list_for_each_entry_safe(msg, tmp, &pg->deferred, node) {
+ list_del(&msg->node);
+ kfree(msg);
+ }
+ pg->num_deferred = 0;
+}
+
static int pmic_glink_rpmsg_callback(struct rpmsg_device *rpdev, void *data,
int len, void *priv, u32 addr)
{
- struct pmic_glink_client *client;
struct pmic_glink_hdr *hdr;
struct pmic_glink *pg = dev_get_drvdata(&rpdev->dev);
- unsigned long flags;
if (len < sizeof(*hdr)) {
dev_warn(pg->dev, "ignoring truncated message\n");
@@ -164,12 +245,12 @@ static int pmic_glink_rpmsg_callback(struct rpmsg_device *rpdev, void *data,
hdr = data;
- spin_lock_irqsave(&pg->client_lock, flags);
- list_for_each_entry(client, &pg->clients, node) {
- if (client->id == le32_to_cpu(hdr->owner))
- client->cb(data, len, client->priv);
- }
- spin_unlock_irqrestore(&pg->client_lock, flags);
+ /* The sender of a request is waiting for the response */
+ if (le32_to_cpu(hdr->type) != PMIC_GLINK_REQ_RESP &&
+ pmic_glink_defer(pg, data, len))
+ return 0;
+
+ pmic_glink_dispatch(pg, data, len);
return 0;
}
@@ -210,6 +291,13 @@ static void pmic_glink_del_aux_device(struct pmic_glink *pg,
auxiliary_device_uninit(aux);
}
+static bool pmic_glink_deferring(struct pmic_glink *pg)
+{
+ guard(spinlock_irqsave)(&pg->defer_lock);
+
+ return pg->defer;
+}
+
static void pmic_glink_state_notify_clients(struct pmic_glink *pg)
{
struct pmic_glink_client *client;
@@ -217,13 +305,18 @@ static void pmic_glink_state_notify_clients(struct pmic_glink *pg)
unsigned long flags;
if (pg->client_state != SERVREG_SERVICE_STATE_UP) {
- if (pg->pdr_state == SERVREG_SERVICE_STATE_UP && pg->ept)
+ /* On UP the clients use the remote, hold that back as well */
+ if (pg->pdr_state == SERVREG_SERVICE_STATE_UP && pg->ept &&
+ !pmic_glink_deferring(pg))
new_state = SERVREG_SERVICE_STATE_UP;
} else {
if (pg->pdr_state == SERVREG_SERVICE_STATE_DOWN || !pg->ept)
new_state = SERVREG_SERVICE_STATE_DOWN;
}
+ if (pg->pdr_state == SERVREG_SERVICE_STATE_DOWN || !pg->ept)
+ pmic_glink_drop_deferred(pg);
+
if (new_state != pg->client_state) {
spin_lock_irqsave(&pg->client_lock, flags);
list_for_each_entry(client, &pg->clients, node)
@@ -233,6 +326,40 @@ static void pmic_glink_state_notify_clients(struct pmic_glink *pg)
}
}
+static void pmic_glink_deliver_work(struct work_struct *work)
+{
+ struct pmic_glink *pg = container_of(work, struct pmic_glink, deliver_work);
+ struct pmic_glink_deferred_msg *msg;
+ unsigned int dropped;
+ unsigned long flags;
+
+ guard(mutex)(&pg->state_lock);
+
+ for (;;) {
+ spin_lock_irqsave(&pg->defer_lock, flags);
+ msg = list_first_entry_or_null(&pg->deferred, struct pmic_glink_deferred_msg, node);
+ if (!msg) {
+ pg->defer = false;
+ dropped = pg->num_dropped;
+ pg->num_dropped = 0;
+ spin_unlock_irqrestore(&pg->defer_lock, flags);
+ break;
+ }
+
+ list_del(&msg->node);
+ pg->num_deferred--;
+ spin_unlock_irqrestore(&pg->defer_lock, flags);
+
+ pmic_glink_dispatch(pg, msg->data, msg->len);
+ kfree(msg);
+ }
+
+ pmic_glink_state_notify_clients(pg);
+
+ if (dropped)
+ dev_warn(pg->dev, "%u notifications lost during system sleep\n", dropped);
+}
+
static void pmic_glink_pdr_callback(int state, char *svc_path, void *priv)
{
struct pmic_glink *pg = priv;
@@ -315,6 +442,10 @@ static int pmic_glink_probe(struct platform_device *pdev)
spin_lock_init(&pg->client_lock);
mutex_init(&pg->state_lock);
+ INIT_LIST_HEAD(&pg->deferred);
+ spin_lock_init(&pg->defer_lock);
+ INIT_WORK(&pg->deliver_work, pmic_glink_deliver_work);
+
pg->data = of_device_get_match_data(&pdev->dev);
if (!pg->data)
return -EINVAL;
@@ -377,6 +508,8 @@ static void pmic_glink_remove(struct platform_device *pdev)
{
struct pmic_glink *pg = dev_get_drvdata(&pdev->dev);
+ cancel_work_sync(&pg->deliver_work);
+
pdr_handle_release(pg->pdr);
if (pg->data->client_mask & BIT(PMIC_GLINK_CLIENT_BATT))
@@ -386,10 +519,35 @@ static void pmic_glink_remove(struct platform_device *pdev)
if (pg->data->client_mask & BIT(PMIC_GLINK_CLIENT_UCSI))
pmic_glink_del_aux_device(pg, &pg->ucsi_aux);
+ pmic_glink_drop_deferred(pg);
+
guard(mutex)(&__pmic_glink_lock);
__pmic_glink = NULL;
}
+static int pmic_glink_prepare(struct device *dev)
+{
+ struct pmic_glink *pg = dev_get_drvdata(dev);
+
+ guard(spinlock_irqsave)(&pg->defer_lock);
+ pg->defer = true;
+
+ return 0;
+}
+
+static void pmic_glink_complete(struct device *dev)
+{
+ struct pmic_glink *pg = dev_get_drvdata(dev);
+
+ /* Runs once the tasks are thawed, all devices have resumed by then */
+ queue_work(system_freezable_wq, &pg->deliver_work);
+}
+
+static const struct dev_pm_ops pmic_glink_pm_ops = {
+ .prepare = pmic_glink_prepare,
+ .complete = pmic_glink_complete,
+};
+
static const struct pmic_glink_data pmic_glink_adsp_data = {
.client_mask = BIT(PMIC_GLINK_CLIENT_BATT) |
BIT(PMIC_GLINK_CLIENT_ALTMODE) |
@@ -418,6 +576,7 @@ static struct platform_driver pmic_glink_driver = {
.driver = {
.name = "qcom_pmic_glink",
.of_match_table = pmic_glink_of_match,
+ .pm = pm_sleep_ptr(&pmic_glink_pm_ops),
},
};
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread* [RFC PATCH 2/4] soc: qcom: pmic_glink: Report wakeup events on behalf of clients
2026-10-22 16:41 [RFC PATCH 0/4] soc: qcom: pmic_glink: Hold back notifications over system sleep Vladyslav Tymoshchuk
2026-10-22 16:41 ` [RFC PATCH 1/4] " Vladyslav Tymoshchuk
@ 2026-10-22 16:41 ` Vladyslav Tymoshchuk
2026-10-22 16:41 ` [RFC PATCH 3/4] usb: typec: ucsi: glink: Wake up the system on connector changes Vladyslav Tymoshchuk
2026-10-22 17:32 ` [RFC PATCH 4/4] power: supply: qcom_battmgr: Allow charger changes to wake up the system Vladyslav Tymoshchuk
3 siblings, 0 replies; 5+ messages in thread
From: Vladyslav Tymoshchuk @ 2026-10-22 16:41 UTC (permalink / raw)
To: Bjorn Andersson, Konrad Dybcio
Cc: Abel Vesa, Heikki Krogerus, Greg Kroah-Hartman,
Sebastian Reichel, Krzysztof Kozlowski, Dmitry Baryshkov,
Casey Connolly, Johan Hovold, Jishnu Prakash, Kamal Wadhwa,
Joonhoe Kim, Daniel J Blueman, Neil Armstrong, Kees Cook,
Gustavo A . R . Silva, linux-arm-msm, linux-usb, linux-pm,
linux-hardening, linux-kernel, Vladyslav Tymoshchuk
The GLINK interrupt is requested with IRQF_NO_SUSPEND, but it is not a
wakeup interrupt: most PMIC GLINK messages, battery status updates for
one, must not wake up the system. A notification that arrives during
system sleep is now held back until the system has resumed, so the
client sees it too late to report it as a wakeup event.
Decide in pmic_glink, when the notification arrives: it is a wakeup
event if the client's device may wake up the system and the client's
wakeup filter, if the client has set one, accepts it. The client
driver picks the default with device_init_wakeup() or
device_set_wakeup_capable(), and user space can change it through
power/wakeup, like for any other device. Report a hard wakeup event,
so that it also brings the system out of suspend-to-idle.
While a wakeup notification is held back, keep the wakeup source of
the client's device active, so that autosleep, or a suspend started
through /sys/power/wakeup_count, does not put the system back to sleep
before the client has seen it. Release it right before the callback
rather than after it, as the client may keep the same wakeup source
active from there.
Responses are not wakeup events, the client is waiting for them.
Keep the device passed to devm_pmic_glink_client_alloc() in the client
for this, and document the function. No in-tree client enables wakeup
on its device yet, so nothing changes for them.
Signed-off-by: Vladyslav Tymoshchuk <tvlad744@gmail.com>
---
I used an AI assistant (Claude) for the analysis and to draft this
patch and its description; I reviewed the result. The testing is
described in the cover letter.
drivers/soc/qcom/pmic_glink.c | 104 ++++++++++++++++++++++++++--
include/linux/soc/qcom/pmic_glink.h | 2 +
2 files changed, 100 insertions(+), 6 deletions(-)
diff --git a/drivers/soc/qcom/pmic_glink.c b/drivers/soc/qcom/pmic_glink.c
index 748b335be6ef..c629df8009e6 100644
--- a/drivers/soc/qcom/pmic_glink.c
+++ b/drivers/soc/qcom/pmic_glink.c
@@ -9,12 +9,14 @@
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
+#include <linux/pm_wakeup.h>
#include <linux/rpmsg.h>
#include <linux/slab.h>
#include <linux/soc/qcom/pdr.h>
#include <linux/soc/qcom/pmic_glink.h>
#include <linux/spinlock.h>
#include <linux/string.h>
+#include <linux/suspend.h>
#include <linux/workqueue.h>
#define PMIC_GLINK_SEND_TIMEOUT (5 * HZ)
@@ -70,11 +72,15 @@ struct pmic_glink_client {
struct list_head node;
struct pmic_glink *pg;
+ struct device *dev;
unsigned int id;
void (*cb)(const void *data, size_t len, void *priv);
void (*pdr_notify)(void *priv, int state);
+ bool (*wakeup_filter)(const void *data, size_t len, void *priv);
void *priv;
+
+ bool stay_awake;
};
struct pmic_glink_deferred_msg {
@@ -83,6 +89,16 @@ struct pmic_glink_deferred_msg {
u8 data[] __counted_by(len);
};
+static void pmic_glink_client_relax(struct pmic_glink_client *client)
+{
+ lockdep_assert_held(&client->pg->client_lock);
+
+ if (client->stay_awake) {
+ client->stay_awake = false;
+ pm_relax(client->dev);
+ }
+}
+
static void _devm_pmic_glink_release_client(struct device *dev, void *res)
{
struct pmic_glink_client *client = (struct pmic_glink_client *)res;
@@ -91,9 +107,26 @@ static void _devm_pmic_glink_release_client(struct device *dev, void *res)
spin_lock_irqsave(&pg->client_lock, flags);
list_del(&client->node);
+ pmic_glink_client_relax(client);
spin_unlock_irqrestore(&pg->client_lock, flags);
}
+/**
+ * devm_pmic_glink_client_alloc() - allocate a client of the PMIC GLINK service
+ * @dev: client device, a child of the pmic_glink device
+ * @id: owner of the messages for the client
+ * @cb: called, in atomic context, with each message for the client
+ * @pdr: called, in atomic context, with the state of the remote service
+ * @priv: passed to @cb, @pdr and the wakeup filter
+ *
+ * A notification for a client whose @dev may wake up the system, see
+ * device_may_wakeup(), is a wakeup event, unless the wakeup filter of the
+ * client rejects it. Notifications that arrive during system sleep reach @cb
+ * once the system has resumed.
+ *
+ * Return: the client, to be registered with pmic_glink_client_register(), or
+ * an ERR_PTR() on failure.
+ */
struct pmic_glink_client *devm_pmic_glink_client_alloc(struct device *dev,
unsigned int id,
void (*cb)(const void *, size_t, void *),
@@ -108,6 +141,7 @@ struct pmic_glink_client *devm_pmic_glink_client_alloc(struct device *dev,
return ERR_PTR(-ENOMEM);
client->pg = pg;
+ client->dev = dev;
client->id = id;
client->cb = cb;
client->pdr_notify = pdr;
@@ -120,6 +154,22 @@ struct pmic_glink_client *devm_pmic_glink_client_alloc(struct device *dev,
}
EXPORT_SYMBOL_GPL(devm_pmic_glink_client_alloc);
+/**
+ * pmic_glink_client_set_wakeup_filter() - select the wakeup notifications
+ * @client: client from devm_pmic_glink_client_alloc(), not registered yet
+ * @filter: called, in atomic context, with each notification for the client
+ * while its device may wake up the system; returns true to wake
+ * the system up
+ *
+ * Without a filter, every notification for the client is a wakeup event.
+ */
+void pmic_glink_client_set_wakeup_filter(struct pmic_glink_client *client,
+ bool (*filter)(const void *, size_t, void *))
+{
+ client->wakeup_filter = filter;
+}
+EXPORT_SYMBOL_GPL(pmic_glink_client_set_wakeup_filter);
+
void pmic_glink_client_register(struct pmic_glink_client *client)
{
struct pmic_glink *pg = client->pg;
@@ -166,6 +216,15 @@ int pmic_glink_send(struct pmic_glink_client *client, void *data, size_t len)
}
EXPORT_SYMBOL_GPL(pmic_glink_send);
+static bool pmic_glink_is_wakeup_event(struct pmic_glink_client *client,
+ const void *data, size_t len)
+{
+ if (!device_may_wakeup(client->dev))
+ return false;
+
+ return !client->wakeup_filter || client->wakeup_filter(data, len, client->priv);
+}
+
static void pmic_glink_dispatch(struct pmic_glink *pg, const void *data, size_t len)
{
const struct pmic_glink_hdr *hdr = data;
@@ -174,8 +233,12 @@ static void pmic_glink_dispatch(struct pmic_glink *pg, const void *data, size_t
spin_lock_irqsave(&pg->client_lock, flags);
list_for_each_entry(client, &pg->clients, node) {
- if (client->id == le32_to_cpu(hdr->owner))
- client->cb(data, len, client->priv);
+ if (client->id != le32_to_cpu(hdr->owner))
+ continue;
+
+ /* From here on, the client keeps the system awake if it needs to */
+ pmic_glink_client_relax(client);
+ client->cb(data, len, client->priv);
}
spin_unlock_irqrestore(&pg->client_lock, flags);
}
@@ -235,8 +298,11 @@ static void pmic_glink_drop_deferred(struct pmic_glink *pg)
static int pmic_glink_rpmsg_callback(struct rpmsg_device *rpdev, void *data,
int len, void *priv, u32 addr)
{
+ struct pmic_glink_client *client;
struct pmic_glink_hdr *hdr;
struct pmic_glink *pg = dev_get_drvdata(&rpdev->dev);
+ bool notification, deferred;
+ unsigned long flags;
if (len < sizeof(*hdr)) {
dev_warn(pg->dev, "ignoring truncated message\n");
@@ -246,11 +312,30 @@ static int pmic_glink_rpmsg_callback(struct rpmsg_device *rpdev, void *data,
hdr = data;
/* The sender of a request is waiting for the response */
- if (le32_to_cpu(hdr->type) != PMIC_GLINK_REQ_RESP &&
- pmic_glink_defer(pg, data, len))
- return 0;
+ notification = le32_to_cpu(hdr->type) != PMIC_GLINK_REQ_RESP;
+
+ /* Defer under client_lock, to set stay_awake before it is delivered */
+ spin_lock_irqsave(&pg->client_lock, flags);
+ deferred = notification && pmic_glink_defer(pg, data, len);
+ list_for_each_entry(client, &pg->clients, node) {
+ if (client->id != le32_to_cpu(hdr->owner))
+ continue;
+
+ if (deferred) {
+ if (pmic_glink_is_wakeup_event(client, data, len)) {
+ pm_stay_awake(client->dev);
+ pm_system_wakeup();
+ client->stay_awake = true;
+ }
+ continue;
+ }
+
+ if (notification && pmic_glink_is_wakeup_event(client, data, len))
+ pm_wakeup_hard_event(client->dev);
- pmic_glink_dispatch(pg, data, len);
+ client->cb(data, len, client->priv);
+ }
+ spin_unlock_irqrestore(&pg->client_lock, flags);
return 0;
}
@@ -330,6 +415,7 @@ static void pmic_glink_deliver_work(struct work_struct *work)
{
struct pmic_glink *pg = container_of(work, struct pmic_glink, deliver_work);
struct pmic_glink_deferred_msg *msg;
+ struct pmic_glink_client *client;
unsigned int dropped;
unsigned long flags;
@@ -354,6 +440,12 @@ static void pmic_glink_deliver_work(struct work_struct *work)
kfree(msg);
}
+ /* Release the clients whose notifications were dropped */
+ spin_lock_irqsave(&pg->client_lock, flags);
+ list_for_each_entry(client, &pg->clients, node)
+ pmic_glink_client_relax(client);
+ spin_unlock_irqrestore(&pg->client_lock, flags);
+
pmic_glink_state_notify_clients(pg);
if (dropped)
diff --git a/include/linux/soc/qcom/pmic_glink.h b/include/linux/soc/qcom/pmic_glink.h
index 7cddf1027752..786d2f35a9a9 100644
--- a/include/linux/soc/qcom/pmic_glink.h
+++ b/include/linux/soc/qcom/pmic_glink.h
@@ -28,6 +28,8 @@ struct pmic_glink_client *devm_pmic_glink_client_alloc(struct device *dev,
void (*cb)(const void *, size_t, void *),
void (*pdr)(void *, int),
void *priv);
+void pmic_glink_client_set_wakeup_filter(struct pmic_glink_client *client,
+ bool (*filter)(const void *, size_t, void *));
void pmic_glink_client_register(struct pmic_glink_client *client);
#endif
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread* [RFC PATCH 3/4] usb: typec: ucsi: glink: Wake up the system on connector changes
2026-10-22 16:41 [RFC PATCH 0/4] soc: qcom: pmic_glink: Hold back notifications over system sleep Vladyslav Tymoshchuk
2026-10-22 16:41 ` [RFC PATCH 1/4] " Vladyslav Tymoshchuk
2026-10-22 16:41 ` [RFC PATCH 2/4] soc: qcom: pmic_glink: Report wakeup events on behalf of clients Vladyslav Tymoshchuk
@ 2026-10-22 16:41 ` Vladyslav Tymoshchuk
2026-10-22 17:32 ` [RFC PATCH 4/4] power: supply: qcom_battmgr: Allow charger changes to wake up the system Vladyslav Tymoshchuk
3 siblings, 0 replies; 5+ messages in thread
From: Vladyslav Tymoshchuk @ 2026-10-22 16:41 UTC (permalink / raw)
To: Bjorn Andersson, Konrad Dybcio
Cc: Abel Vesa, Heikki Krogerus, Greg Kroah-Hartman,
Sebastian Reichel, Krzysztof Kozlowski, Dmitry Baryshkov,
Casey Connolly, Johan Hovold, Jishnu Prakash, Kamal Wadhwa,
Joonhoe Kim, Daniel J Blueman, Neil Armstrong, Kees Cook,
Gustavo A . R . Silva, linux-arm-msm, linux-usb, linux-pm,
linux-hardening, linux-kernel, Vladyslav Tymoshchuk
The UCSI notifications from PMIC GLINK report connector changes, a USB
cable being plugged in or out for one, and the completion of UCSI
commands. Enable wakeup on the device, so that pmic_glink reports them
as wakeup events: a cable change brings the system out of
suspend-to-idle and aborts a suspend in progress.
The completion of a command arrives only while the command is in
flight, so it can wake up the system only when a suspend has started
in the middle of one.
The same was proposed in [1], with the wakeup event reported from the
callback. pmic_glink reports it now, also for the notifications that
it holds back over system sleep.
Link: https://lore.kernel.org/all/20260722-ucsi_glink_wakeup-v2-1-b43dee56df24@oss.qualcomm.com/ [1]
Signed-off-by: Vladyslav Tymoshchuk <tvlad744@gmail.com>
---
I used an AI assistant (Claude) for the analysis and to draft this
patch and its description; I reviewed the result. The testing is
described in the cover letter.
drivers/usb/typec/ucsi/ucsi_glink.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/drivers/usb/typec/ucsi/ucsi_glink.c b/drivers/usb/typec/ucsi/ucsi_glink.c
index 1db0c88d817e..b03320f5a6bb 100644
--- a/drivers/usb/typec/ucsi/ucsi_glink.c
+++ b/drivers/usb/typec/ucsi/ucsi_glink.c
@@ -7,6 +7,7 @@
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/of_device.h>
+#include <linux/pm_wakeup.h>
#include <linux/property.h>
#include <linux/soc/qcom/pdr.h>
#include <linux/usb/typec_mux.h>
@@ -455,6 +456,11 @@ static int pmic_glink_ucsi_probe(struct auxiliary_device *adev,
ucsi->port_orientation[port] = desc;
}
+ /* pmic_glink reports the notifications as wakeup events */
+ ret = devm_device_init_wakeup(dev);
+ if (ret)
+ return ret;
+
ucsi->client = devm_pmic_glink_client_alloc(dev, PMIC_GLINK_OWNER_USBC,
pmic_glink_ucsi_callback,
pmic_glink_ucsi_pdr_notify,
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [RFC PATCH 4/4] power: supply: qcom_battmgr: Allow charger changes to wake up the system
2026-10-22 16:41 [RFC PATCH 0/4] soc: qcom: pmic_glink: Hold back notifications over system sleep Vladyslav Tymoshchuk
` (2 preceding siblings ...)
2026-10-22 16:41 ` [RFC PATCH 3/4] usb: typec: ucsi: glink: Wake up the system on connector changes Vladyslav Tymoshchuk
@ 2026-10-22 17:32 ` Vladyslav Tymoshchuk
3 siblings, 0 replies; 5+ messages in thread
From: Vladyslav Tymoshchuk @ 2026-10-22 17:32 UTC (permalink / raw)
To: Bjorn Andersson, Konrad Dybcio
Cc: Abel Vesa, Heikki Krogerus, Greg Kroah-Hartman,
Sebastian Reichel, Krzysztof Kozlowski, Dmitry Baryshkov,
Casey Connolly, Johan Hovold, Jishnu Prakash, Kamal Wadhwa,
Joonhoe Kim, Daniel J Blueman, Neil Armstrong, Kees Cook,
Gustavo A . R . Silva, linux-arm-msm, linux-usb, linux-pm,
linux-hardening, linux-kernel, Vladyslav Tymoshchuk
The battery manager notifies about changes of the battery state, many
of which arrive while the system is asleep, and about changes of the
USB and wireless charger properties, a charger being plugged in or out
for one. Only the latter can be worth a wakeup, to show the new
charging state for example.
Make the device wakeup capable, and set a wakeup filter that lets
pmic_glink report only the charger notifications as wakeup events.
Leave wakeup disabled, so that nothing changes by default: whether a
charger change should wake up the system is up to user space, through
power/wakeup.
Signed-off-by: Vladyslav Tymoshchuk <tvlad744@gmail.com>
---
I used an AI assistant (Claude) for the analysis and to draft this
patch and its description; I reviewed the result. The testing is
described in the cover letter.
drivers/power/supply/qcom_battmgr.c | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/drivers/power/supply/qcom_battmgr.c b/drivers/power/supply/qcom_battmgr.c
index 636d6f6acecc..7a783a3a320d 100644
--- a/drivers/power/supply/qcom_battmgr.c
+++ b/drivers/power/supply/qcom_battmgr.c
@@ -10,6 +10,7 @@
#include <linux/mutex.h>
#include <linux/nvmem-consumer.h>
#include <linux/of_device.h>
+#include <linux/pm_wakeup.h>
#include <linux/power_supply.h>
#include <linux/property.h>
#include <linux/soc/qcom/pdr.h>
@@ -1596,6 +1597,20 @@ static void qcom_battmgr_sm8350_callback(struct qcom_battmgr *battmgr,
complete(&battmgr->ack);
}
+static bool qcom_battmgr_is_charger_notification(const void *data, size_t len, void *priv)
+{
+ const struct qcom_battmgr_message *msg = data;
+ unsigned int notification;
+
+ if (le32_to_cpu(msg->hdr.opcode) != BATTMGR_NOTIFICATION ||
+ len != sizeof(msg->hdr) + sizeof(msg->notification))
+ return false;
+
+ notification = le32_to_cpu(msg->notification) & 0xff;
+
+ return notification == NOTIF_USB_PROPERTY || notification == NOTIF_WLS_PROPERTY;
+}
+
static void qcom_battmgr_callback(const void *data, size_t len, void *priv)
{
const struct pmic_glink_hdr *hdr = data;
@@ -1750,6 +1765,10 @@ static int qcom_battmgr_probe(struct auxiliary_device *adev,
if (IS_ERR(battmgr->client))
return PTR_ERR(battmgr->client);
+ device_set_wakeup_capable(dev, true);
+ pmic_glink_client_set_wakeup_filter(battmgr->client,
+ qcom_battmgr_is_charger_notification);
+
pmic_glink_client_register(battmgr->client);
return 0;
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread