From 6d2a3a07360b4179b290ab7717d0ede1c6706db5 Mon Sep 17 00:00:00 2001 From: "Yew, Chang Ching" Date: Sun, 5 Apr 2026 05:45:53 +0800 Subject: [PATCH 1/5] maxim-serdes: Add suspend and resume to max96717 and max96724 Add system sleep callbacks for the MAX96724 deserializer and the MAX96717 serializer. Use the common serdes suspend helpers to preserve state on suspend, and restore device access and common serdes state on resume before streaming restarts. Signed-off-by: Yew, Chang Ching Signed-off-by: Sodhi, Vunny --- drivers/media/i2c/maxim-serdes/max96717.c | 28 ++++++++++++- drivers/media/i2c/maxim-serdes/max96724.c | 23 +++++++++++ drivers/media/i2c/maxim-serdes/max_des.c | 37 +++++++++++++++++ drivers/media/i2c/maxim-serdes/max_des.h | 4 ++ drivers/media/i2c/maxim-serdes/max_ser.c | 50 +++++++++++++++++++++++ drivers/media/i2c/maxim-serdes/max_ser.h | 4 ++ 6 files changed, 145 insertions(+), 1 deletion(-) diff --git a/drivers/media/i2c/maxim-serdes/max96717.c b/drivers/media/i2c/maxim-serdes/max96717.c index a8733e80..68fa1afd 100644 --- a/drivers/media/i2c/maxim-serdes/max96717.c +++ b/drivers/media/i2c/maxim-serdes/max96717.c @@ -268,7 +268,7 @@ static const struct regmap_config max96717_i2c_regmap = { static int max96717_wait_for_device(struct max96717_priv *priv) { unsigned int i; - int ret; + int ret = 0; for (i = 0; i < 10; i++) { unsigned int val; @@ -277,6 +277,9 @@ static int max96717_wait_for_device(struct max96717_priv *priv) if (!ret && val) return 0; + if (!ret) + ret = -ETIMEDOUT; + msleep(100); dev_err(priv->dev, "Retry %u waiting for serializer: %d\n", i, ret); @@ -1810,6 +1813,28 @@ static void max96717_remove(struct i2c_client *client) max_ser_remove(&priv->ser); } +static int max96717_suspend(struct device *dev) +{ + struct max96717_priv *priv = dev_get_drvdata(dev); + + return max_ser_suspend(&priv->ser); +} + +static int max96717_resume(struct device *dev) +{ + struct max96717_priv *priv = dev_get_drvdata(dev); + int ret; + + ret = max96717_wait_for_device(priv); + if (ret) + return ret; + + return max_ser_resume(&priv->ser); +} + +static DEFINE_SIMPLE_DEV_PM_OPS(max96717_pm_ops, + max96717_suspend, max96717_resume); + static const struct max96717_chip_info max9295a_info = { .ops = &max96717_common_ops, .modes = BIT(MAX_SERDES_GMSL_PIXEL_MODE), @@ -1876,6 +1901,7 @@ static struct i2c_driver max96717_i2c_driver = { .name = MAX96717_NAME, .of_match_table = max96717_of_ids, .acpi_match_table = max96717_acpi_ids, + .pm = pm_sleep_ptr(&max96717_pm_ops), }, .probe = max96717_probe, .remove = max96717_remove, diff --git a/drivers/media/i2c/maxim-serdes/max96724.c b/drivers/media/i2c/maxim-serdes/max96724.c index 625c7956..13791b5e 100644 --- a/drivers/media/i2c/maxim-serdes/max96724.c +++ b/drivers/media/i2c/maxim-serdes/max96724.c @@ -1169,6 +1169,28 @@ static void max96724_remove(struct i2c_client *client) gpiod_set_value_cansleep(priv->gpiod_enable, 0); } +static int max96724_suspend(struct device *dev) +{ + struct max96724_priv *priv = dev_get_drvdata(dev); + + return max_des_suspend(&priv->des); +} + +static int max96724_resume(struct device *dev) +{ + struct max96724_priv *priv = dev_get_drvdata(dev); + int ret; + + ret = max96724_reset(priv); + if (ret) + return ret; + + return max_des_resume(&priv->des); +} + +static DEFINE_SIMPLE_DEV_PM_OPS(max96724_pm_ops, + max96724_suspend, max96724_resume); + static const struct acpi_device_id max96724_acpi_ids[] = { { "INTC1139", (kernel_ulong_t) &max96724_info }, {} @@ -1189,6 +1211,7 @@ static struct i2c_driver max96724_i2c_driver = { .name = "max96724", .of_match_table = max96724_of_table, .acpi_match_table = max96724_acpi_ids, + .pm = pm_sleep_ptr(&max96724_pm_ops), }, .probe = max96724_probe, .remove = max96724_remove, diff --git a/drivers/media/i2c/maxim-serdes/max_des.c b/drivers/media/i2c/maxim-serdes/max_des.c index 017950c2..a3a0c9d5 100644 --- a/drivers/media/i2c/maxim-serdes/max_des.c +++ b/drivers/media/i2c/maxim-serdes/max_des.c @@ -3473,5 +3473,42 @@ int max_des_remove(struct max_des *des) } EXPORT_SYMBOL_NS_GPL(max_des_remove, "MAX_SERDES"); +int max_des_suspend(struct max_des *des) +{ + struct max_des_priv *priv = des->priv; + + if (des->ops->set_enable) + des->ops->set_enable(des, false); + + max_des_update_pocs(priv, false); + + return 0; +} +EXPORT_SYMBOL_NS_GPL(max_des_suspend, "MAX_SERDES"); + +int max_des_resume(struct max_des *des) +{ + struct max_des_priv *priv = des->priv; + int ret; + + ret = max_des_update_pocs(priv, true); + if (ret) + return ret; + + ret = max_des_init(priv); + if (ret) { + dev_err(priv->dev, "Failed to re-initialize deserializer: %d\n", ret); + goto err_disable_pocs; + } + + return 0; + +err_disable_pocs: + max_des_update_pocs(priv, false); + + return ret; +} +EXPORT_SYMBOL_NS_GPL(max_des_resume, "MAX_SERDES"); + MODULE_LICENSE("GPL"); MODULE_IMPORT_NS("I2C_ATR"); diff --git a/drivers/media/i2c/maxim-serdes/max_des.h b/drivers/media/i2c/maxim-serdes/max_des.h index c495898d..a2d760ea 100644 --- a/drivers/media/i2c/maxim-serdes/max_des.h +++ b/drivers/media/i2c/maxim-serdes/max_des.h @@ -151,6 +151,10 @@ int max_des_probe(struct i2c_client *client, struct max_des *des); int max_des_remove(struct max_des *des); +int max_des_suspend(struct max_des *des); + +int max_des_resume(struct max_des *des); + int max_des_phy_hw_data_lanes(struct max_des *des, struct max_des_phy *phy); #endif // MAX_DES_H diff --git a/drivers/media/i2c/maxim-serdes/max_ser.c b/drivers/media/i2c/maxim-serdes/max_ser.c index 4b05a561..833880a4 100644 --- a/drivers/media/i2c/maxim-serdes/max_ser.c +++ b/drivers/media/i2c/maxim-serdes/max_ser.c @@ -2232,6 +2232,56 @@ int max_ser_remove(struct max_ser *ser) } EXPORT_SYMBOL_NS_GPL(max_ser_remove, "MAX_SERDES"); +int max_ser_suspend(struct max_ser *ser) +{ + struct max_ser_priv *priv = ser->priv; + unsigned int i; + + for (i = 0; i < ser->ops->num_phys; i++) { + struct max_ser_phy *phy = &ser->phys[i]; + + if (ser->ops->set_phy_active && phy->active) { + ser->ops->set_phy_active(ser, phy, false); + /* + * Keep the active flag so that resume knows + * which PHYs to re-enable. + */ + } + } + + for (i = 0; i < ser->ops->num_pipes; i++) { + struct max_ser_pipe *pipe = &ser->pipes[i]; + + if (pipe->enabled) + ser->ops->set_pipe_enable(ser, pipe, false); + } + + if (ser->ops->set_tunnel_enable) + ser->ops->set_tunnel_enable(ser, false); + + dev_dbg(priv->dev, "Serializer suspended\n"); + + return 0; +} +EXPORT_SYMBOL_NS_GPL(max_ser_suspend, "MAX_SERDES"); + +int max_ser_resume(struct max_ser *ser) +{ + struct max_ser_priv *priv = ser->priv; + int ret; + + ret = max_ser_init(priv); + if (ret) { + dev_err(priv->dev, "Failed to re-initialize serializer: %d\n", ret); + return ret; + } + + dev_dbg(priv->dev, "Serializer resumed\n"); + + return 0; +} +EXPORT_SYMBOL_NS_GPL(max_ser_resume, "MAX_SERDES"); + int max_ser_set_double_bpps(struct v4l2_subdev *sd, u32 double_bpps) { struct max_ser_priv *priv = sd_to_priv(sd); diff --git a/drivers/media/i2c/maxim-serdes/max_ser.h b/drivers/media/i2c/maxim-serdes/max_ser.h index eb5243f0..b57776ec 100644 --- a/drivers/media/i2c/maxim-serdes/max_ser.h +++ b/drivers/media/i2c/maxim-serdes/max_ser.h @@ -143,6 +143,10 @@ int max_ser_probe(struct i2c_client *client, struct max_ser *ser); int max_ser_remove(struct max_ser *ser); +int max_ser_suspend(struct max_ser *ser); + +int max_ser_resume(struct max_ser *ser); + int max_ser_set_double_bpps(struct v4l2_subdev *sd, u32 double_bpps); unsigned int max_ser_get_supported_modes(struct v4l2_subdev *sd); int max_ser_set_mode(struct v4l2_subdev *sd, enum max_serdes_gmsl_mode mode); From bf0181b6c0dc45a5f4ef16b669007cdb0b905603 Mon Sep 17 00:00:00 2001 From: "Sodhi, Vunny" Date: Fri, 17 Jul 2026 10:23:30 +0800 Subject: [PATCH 2/5] media: maxim-serdes: add S3/S4 PM suspend/resume support Restore minimal hardware state on resume: - Deserializer: restore serializer I2C alias translations and re-run select_links() with RESET_ONESHOT for forwarding pipeline - Serializer: restore cached I2C translations and tunnel mode (if active) - Set resume_reconfigure_pending flag to bypass stale software cache and force full hardware reprogram on next stream start The implementation follows the standard Linux PM model: suspend quiesces hardware, resume reinitializes to clean baseline, and enable_streams() reprograms full runtime configuration when userspace restarts streaming. Signed-off-by: Sodhi, Vunny --- drivers/media/i2c/maxim-serdes/max96717.c | 5 +- drivers/media/i2c/maxim-serdes/max_des.c | 96 ++++++++++++++++++++--- drivers/media/i2c/maxim-serdes/max_ser.c | 56 ++++++++++--- 3 files changed, 134 insertions(+), 23 deletions(-) diff --git a/drivers/media/i2c/maxim-serdes/max96717.c b/drivers/media/i2c/maxim-serdes/max96717.c index 68fa1afd..9fc72fd2 100644 --- a/drivers/media/i2c/maxim-serdes/max96717.c +++ b/drivers/media/i2c/maxim-serdes/max96717.c @@ -268,7 +268,7 @@ static const struct regmap_config max96717_i2c_regmap = { static int max96717_wait_for_device(struct max96717_priv *priv) { unsigned int i; - int ret = 0; + int ret; for (i = 0; i < 10; i++) { unsigned int val; @@ -277,9 +277,6 @@ static int max96717_wait_for_device(struct max96717_priv *priv) if (!ret && val) return 0; - if (!ret) - ret = -ETIMEDOUT; - msleep(100); dev_err(priv->dev, "Retry %u waiting for serializer: %d\n", i, ret); diff --git a/drivers/media/i2c/maxim-serdes/max_des.c b/drivers/media/i2c/maxim-serdes/max_des.c index a3a0c9d5..7a3c61ae 100644 --- a/drivers/media/i2c/maxim-serdes/max_des.c +++ b/drivers/media/i2c/maxim-serdes/max_des.c @@ -50,6 +50,9 @@ struct max_des_priv { s64 link_freq_menu[1]; struct max_des_phy *unused_phy; + + /* Force a full hardware reprogram on first post-resume stream update. */ + bool resume_reconfigure_pending; }; struct max_des_remap_context { @@ -929,7 +932,8 @@ static int max_des_set_modes(struct max_des_priv *priv, max_des_get_phy_mode(context, phy, &mode); - if (phy->mode.alt_mem_map8 == mode.alt_mem_map8 && + if (!priv->resume_reconfigure_pending && + phy->mode.alt_mem_map8 == mode.alt_mem_map8 && phy->mode.alt_mem_map10 == mode.alt_mem_map10 && phy->mode.alt_mem_map12 == mode.alt_mem_map12 && phy->mode.alt2_mem_map8 == mode.alt2_mem_map8) @@ -950,7 +954,8 @@ static int max_des_set_modes(struct max_des_priv *priv, max_des_get_pipe_mode(context, pipe, &mode); - if (pipe->mode.dbl8 == mode.dbl8 && + if (!priv->resume_reconfigure_pending && + pipe->mode.dbl8 == mode.dbl8 && pipe->mode.dbl10 == mode.dbl10 && pipe->mode.dbl12 == mode.dbl12 && pipe->mode.dbl8mode == mode.dbl8mode && @@ -1452,7 +1457,8 @@ static int max_des_update_pipe_remaps(struct max_des_priv *priv, * packet errors) and can wedge the capture. Only reprogram on a real * change. */ - if (pipe->remaps && pipe->num_remaps == num_remaps && + if (!priv->resume_reconfigure_pending && + pipe->remaps && pipe->num_remaps == num_remaps && !memcmp(pipe->remaps, remaps, num_remaps * sizeof(*remaps))) { devm_kfree(priv->dev, remaps); return 0; @@ -1503,7 +1509,7 @@ static int max_des_update_pipe_enable(struct max_des_priv *priv, break; } - if (enable == pipe->enabled) + if (!priv->resume_reconfigure_pending && enable == pipe->enabled) return 0; /* @@ -2528,7 +2534,8 @@ static int max_des_update_active(struct max_des_priv *priv, u64 *streams_masks, } } - if (active != expected_active || des->active == active) + if (active != expected_active || + (!priv->resume_reconfigure_pending && des->active == active)) return 0; if (des->ops->set_enable) { @@ -2673,6 +2680,7 @@ static int max_des_update_streams(struct v4l2_subdev *sd, devm_kfree(priv->dev, priv->streams_masks); priv->streams_masks = streams_masks; + priv->resume_reconfigure_pending = false; return 0; @@ -3476,24 +3484,71 @@ EXPORT_SYMBOL_NS_GPL(max_des_remove, "MAX_SERDES"); int max_des_suspend(struct max_des *des) { struct max_des_priv *priv = des->priv; + int ret; - if (des->ops->set_enable) - des->ops->set_enable(des, false); + if (des->ops->set_enable) { + ret = des->ops->set_enable(des, false); + if (ret) + dev_warn(priv->dev, "suspend: set_enable(false) failed: %d\n", ret); + } - max_des_update_pocs(priv, false); + ret = max_des_update_pocs(priv, false); + if (ret) { + dev_err(priv->dev, "suspend: failed to disable POCs: %d\n", ret); + return ret; + } return 0; } EXPORT_SYMBOL_NS_GPL(max_des_suspend, "MAX_SERDES"); +static void max_des_resume_restore_serializer_aliases(struct max_des_priv *priv) +{ + struct max_des *des = priv->des; + unsigned int i; + + if (!des->ops->select_links) + return; + + for (i = 0; i < des->ops->num_links; i++) { + struct max_des_link *link = &des->links[i]; + int ret; + + if (!link->enabled || !link->ser_xlate.en) + continue; + + ret = max_des_init_link_ser_xlate(priv, link, + priv->client->adapter, + link->ser_xlate.dst, + link->ser_xlate.src); + if (ret) { + /* Retry once after link stabilizes */ + msleep(100); + ret = max_des_init_link_ser_xlate(priv, link, + priv->client->adapter, + link->ser_xlate.dst, + link->ser_xlate.src); + if (ret) + dev_err(priv->dev, + "resume: failed to restore serializer alias on link %u: %d\n", + link->index, ret); + } + } +} + int max_des_resume(struct max_des *des) { struct max_des_priv *priv = des->priv; + unsigned int mask = 0; + unsigned int i; int ret; ret = max_des_update_pocs(priv, true); - if (ret) + if (ret) { + dev_err(priv->dev, + "resume: failed to enable POC supplies: %d\n", ret); return ret; + } ret = max_des_init(priv); if (ret) { @@ -3501,6 +3556,29 @@ int max_des_resume(struct max_des *des) goto err_disable_pocs; } + /* S4: serializer resets to power-up address; restore I2C translations */ + max_des_resume_restore_serializer_aliases(priv); + + /* Restore link mask and issue RESET_ONESHOT for forwarding pipeline */ + if (des->ops->select_links) { + for (i = 0; i < des->ops->num_links; i++) { + if (des->links[i].enabled) + mask |= BIT(i); + } + if (mask) { + ret = des->ops->select_links(des, mask); + if (ret) { + dev_err(priv->dev, + "resume: failed to select links (mask=0x%x): %d\n", + mask, ret); + goto err_disable_pocs; + } + } + } + + /* Force full reprogram on next stream start (HW reset, SW cache stale) */ + priv->resume_reconfigure_pending = true; + return 0; err_disable_pocs: diff --git a/drivers/media/i2c/maxim-serdes/max_ser.c b/drivers/media/i2c/maxim-serdes/max_ser.c index 833880a4..8b1f2260 100644 --- a/drivers/media/i2c/maxim-serdes/max_ser.c +++ b/drivers/media/i2c/maxim-serdes/max_ser.c @@ -1770,6 +1770,42 @@ static int max_ser_init(struct max_ser_priv *priv) return 0; } +static int max_ser_restore_i2c_xlates(struct max_ser_priv *priv) +{ + struct max_ser *ser = priv->ser; + unsigned int i; + int ret; + + for (i = 0; i < ser->ops->num_i2c_xlates; i++) { + struct max_serdes_i2c_xlate *xlate = &ser->i2c_xlates[i]; + + if (!xlate->en) + continue; + + ret = ser->ops->set_i2c_xlate(ser, i, xlate); + if (ret) { + dev_err(priv->dev, + "resume: failed to restore i2c xlate[%u] 0x%02x->0x%02x: %d\n", + i, xlate->src, xlate->dst, ret); + return ret; + } + } + + return 0; +} + +static int max_ser_restore_runtime_state(struct max_ser_priv *priv) +{ + struct max_ser *ser = priv->ser; + + /* Restore tunnel mode; other state reprogrammed by enable_streams() */ + if (ser->ops->set_tunnel_enable && + ser->mode == MAX_SERDES_GMSL_TUNNEL_MODE) + return ser->ops->set_tunnel_enable(ser, true); + + return 0; +} + static int max_ser_notify_bound(struct v4l2_async_notifier *nf, struct v4l2_subdev *subdev, struct v4l2_async_connection *base_asc) @@ -2240,25 +2276,17 @@ int max_ser_suspend(struct max_ser *ser) for (i = 0; i < ser->ops->num_phys; i++) { struct max_ser_phy *phy = &ser->phys[i]; - if (ser->ops->set_phy_active && phy->active) { + if (ser->ops->set_phy_active && phy->active) ser->ops->set_phy_active(ser, phy, false); - /* - * Keep the active flag so that resume knows - * which PHYs to re-enable. - */ - } } for (i = 0; i < ser->ops->num_pipes; i++) { struct max_ser_pipe *pipe = &ser->pipes[i]; - if (pipe->enabled) + if (ser->ops->set_pipe_enable && pipe->enabled) ser->ops->set_pipe_enable(ser, pipe, false); } - if (ser->ops->set_tunnel_enable) - ser->ops->set_tunnel_enable(ser, false); - dev_dbg(priv->dev, "Serializer suspended\n"); return 0; @@ -2276,6 +2304,14 @@ int max_ser_resume(struct max_ser *ser) return ret; } + ret = max_ser_restore_i2c_xlates(priv); + if (ret) + return ret; + + ret = max_ser_restore_runtime_state(priv); + if (ret) + return ret; + dev_dbg(priv->dev, "Serializer resumed\n"); return 0; From 6f60eb8023d777bad0155ac01328ddcc4e05e0a0 Mon Sep 17 00:00:00 2001 From: "Sodhi, Vunny" Date: Tue, 28 Jul 2026 18:36:28 +0800 Subject: [PATCH 3/5] media: maxim-serdes: max_des: use ser_attach_addr on resume On resume, reuse max_des_ser_attach_addr() in re-attach mode instead of the open-coded max_des_resume_restore_serializer_aliases() helper. This consolidates the retry logic and GMSL version pinning in one place and removes the now-redundant helper function. Shorten verbose block comments to single-line comments. Signed-off-by: Sodhi, Vunny --- drivers/media/i2c/maxim-serdes/max_des.c | 86 +++++++++++------------- 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/drivers/media/i2c/maxim-serdes/max_des.c b/drivers/media/i2c/maxim-serdes/max_des.c index 7a3c61ae..cfd8f168 100644 --- a/drivers/media/i2c/maxim-serdes/max_des.c +++ b/drivers/media/i2c/maxim-serdes/max_des.c @@ -1766,15 +1766,22 @@ static int max_des_ser_attach_addr(struct max_des_priv *priv, u32 chan_id, { struct max_des *des = priv->des; struct max_des_link *link = &des->links[chan_id]; + bool reattach = link->ser_xlate.en; int i, min, max; int ret = 0; - max_des_ser_find_version_range(des, &min, &max); + if (reattach) { + /* Resume: reuse negotiated version, reprogram alias after HW reset. */ + min = max = link->version; + } else { + if (link->ser_xlate.en) { + dev_err(priv->dev, + "Serializer for link %u already bound\n", + link->index); + return -EINVAL; + } - if (link->ser_xlate.en) { - dev_err(priv->dev, "Serializer for link %u already bound\n", - link->index); - return -EINVAL; + max_des_ser_find_version_range(des, &min, &max); } for (i = max; i >= min; i--) { @@ -1791,6 +1798,16 @@ static int max_des_ser_attach_addr(struct max_des_priv *priv, u32 chan_id, addr, alias); if (!ret) break; + + if (reattach) { + /* Retry once after link stabilizes on resume. */ + msleep(100); + ret = max_des_init_link_ser_xlate(priv, link, + priv->client->adapter, + addr, alias); + if (!ret) + break; + } } if (ret) { @@ -3502,40 +3519,6 @@ int max_des_suspend(struct max_des *des) } EXPORT_SYMBOL_NS_GPL(max_des_suspend, "MAX_SERDES"); -static void max_des_resume_restore_serializer_aliases(struct max_des_priv *priv) -{ - struct max_des *des = priv->des; - unsigned int i; - - if (!des->ops->select_links) - return; - - for (i = 0; i < des->ops->num_links; i++) { - struct max_des_link *link = &des->links[i]; - int ret; - - if (!link->enabled || !link->ser_xlate.en) - continue; - - ret = max_des_init_link_ser_xlate(priv, link, - priv->client->adapter, - link->ser_xlate.dst, - link->ser_xlate.src); - if (ret) { - /* Retry once after link stabilizes */ - msleep(100); - ret = max_des_init_link_ser_xlate(priv, link, - priv->client->adapter, - link->ser_xlate.dst, - link->ser_xlate.src); - if (ret) - dev_err(priv->dev, - "resume: failed to restore serializer alias on link %u: %d\n", - link->index, ret); - } - } -} - int max_des_resume(struct max_des *des) { struct max_des_priv *priv = des->priv; @@ -3556,15 +3539,28 @@ int max_des_resume(struct max_des *des) goto err_disable_pocs; } - /* S4: serializer resets to power-up address; restore I2C translations */ - max_des_resume_restore_serializer_aliases(priv); - - /* Restore link mask and issue RESET_ONESHOT for forwarding pipeline */ + /* Resume: re-negotiate serializer addresses and restore link mask. */ if (des->ops->select_links) { for (i = 0; i < des->ops->num_links; i++) { - if (des->links[i].enabled) - mask |= BIT(i); + struct max_des_link *link = &des->links[i]; + + if (!link->enabled) + continue; + + mask |= BIT(i); + + if (!link->ser_xlate.en) + continue; + + ret = max_des_ser_attach_addr(priv, link->index, + link->ser_xlate.dst, + link->ser_xlate.src); + if (ret) + dev_err(priv->dev, + "resume: failed to restore serializer alias on link %u: %d\n", + link->index, ret); } + if (mask) { ret = des->ops->select_links(des, mask); if (ret) { From bcae772c76a10eb7c262f11af411c66f5f7d435c Mon Sep 17 00:00:00 2001 From: "Sodhi, Vunny" Date: Fri, 24 Jul 2026 10:14:25 +0800 Subject: [PATCH 4/5] ci: add DKMS build workflow Runs dkms build against multiple kernel families on self-hosted Linux runners: Ubuntu 24.04: 6.12-intel, 6.17-intel, 6.18-intel, 6.17, 7.0 Ubuntu 26.04: 7.0 -intel rows pull headers from the public Intel Linux Overlay (download.01.org) and canonical rows use only Ubuntu archives, with 6.17 / 7.0 arriving via HWE-edge and OEM meta packages. POST_ADD is neutered during 'dkms add'; the upstream source paths extracted for the OOT build are derived from dkms.conf's POST_ADD+= lines so this workflow stays in sync when dkms.conf changes. Signed-off-by: Sodhi, Vunny --- .github/workflows/dkms-build.yml | 164 +++++++++++++++++++++++++++++++ script/dkms-kernel-source.sh | 54 +++++++++- 2 files changed, 213 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/dkms-build.yml diff --git a/.github/workflows/dkms-build.yml b/.github/workflows/dkms-build.yml new file mode 100644 index 00000000..a345906e --- /dev/null +++ b/.github/workflows/dkms-build.yml @@ -0,0 +1,164 @@ +name: dkms build + +on: + push: + pull_request: + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + ubuntu-dkms: + name: Ubuntu ${{ matrix.os_version }} DKMS (${{ matrix.kernel_family }}) + runs-on: [self-hosted, linux] + container: ubuntu:${{ matrix.os_version }} + env: + DEBIAN_FRONTEND: noninteractive + KERNEL_FAMILY: ${{ matrix.kernel_family }} + strategy: + fail-fast: false + matrix: + include: + - { os_version: '24.04', kernel_family: '6.12-intel' } + - { os_version: '24.04', kernel_family: '6.17-intel' } + - { os_version: '24.04', kernel_family: '6.18-intel' } + - { os_version: '24.04', kernel_family: '6.17' } + - { os_version: '24.04', kernel_family: '7.0' } + - { os_version: '26.04', kernel_family: '7.0' } + steps: + - name: Bootstrap git for checkout + run: | + apt-get update -q + apt-get install -y --no-install-recommends git ca-certificates + + - name: Checkout + uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Prepare environment + run: | + set -euo pipefail + + # -intel matrix rows need the public Intel Linux Overlay (01.org). + # Canonical rows use only the Ubuntu default archives. + if [[ "${KERNEL_FAMILY}" == *-intel ]]; then + apt-get install -y --no-install-recommends curl gnupg + curl -fsSL https://download.01.org/intel-linux-overlay/ubuntu/E6FA98203588250569758E97D176E3162086EE4C.gpg \ + | gpg --dearmor >/usr/share/keyrings/intel-linux-overlay.gpg + echo 'deb [signed-by=/usr/share/keyrings/intel-linux-overlay.gpg] https://download.01.org/intel-linux-overlay/ubuntu noble main kernels non-free multimedia' \ + >/etc/apt/sources.list.d/intel-linux-overlay.list + fi + + codename=$(. /etc/os-release && echo "$VERSION_CODENAME") + sed -i "s/${codename}-updates/${codename}-updates ${codename}-proposed/" /etc/apt/sources.list.d/ubuntu.sources + apt-get update -q + apt-get install -y --no-install-recommends \ + build-essential ca-certificates curl dkms git kmod patch wget xz-utils + + - name: Download header files + env: + OS_VERSION: ${{ matrix.os_version }} + run: | + set -euo pipefail + if [[ "${KERNEL_FAMILY}" == *-intel ]]; then + apt-get install -y "linux-headers-${KERNEL_FAMILY}" + fi + # 24.04 LTS needs HWE-edge / OEM meta-packages for newer kernels. + # 26.04+ ship newer kernels natively via linux-headers-generic. + if [[ "${OS_VERSION}" == "24.04" ]]; then + apt-get install -y \ + linux-headers-generic \ + linux-headers-generic-hwe-24.04-edge \ + linux-headers-oem-24.04a \ + linux-headers-oem-24.04b + else + apt-get install -y linux-headers-generic + fi + + - name: Compile driver + run: | + set -euo pipefail + expect_intel=0 + target_family="${KERNEL_FAMILY}" + if [[ "${KERNEL_FAMILY}" == *-intel ]]; then + expect_intel=1 + target_family="${KERNEL_FAMILY%-intel}" + fi + target_regex="^${target_family//./\\.}([-.+].*|[a-z].*)?$" + + package_name="$(sed -n 's/^PACKAGE_NAME="\(.*\)"/\1/p' dkms.conf)" + package_version="$(sed -n 's/^PACKAGE_VERSION="\(.*\)"/\1/p' dkms.conf)" + [[ -n "${package_name}" && -n "${package_version}" ]] \ + || { echo "Failed to parse PACKAGE_NAME/PACKAGE_VERSION from dkms.conf" >&2; exit 1; } + + # Derive upstream kernel source paths from dkms.conf POST_ADD+= lines + # so this workflow stays in sync when dkms.conf changes. + mapfile -t post_add_paths < <( + awk -F'"' '/^POST_ADD\+=/ { + n = split($2, a, " ") + for (i = 1; i <= n; i++) if (a[i] != "" && !seen[a[i]]++) print a[i] + }' dkms.conf + ) + (( ${#post_add_paths[@]} > 0 )) \ + || { echo "Failed to derive post_add_paths from dkms.conf" >&2; exit 1; } + echo "Derived post_add_paths from dkms.conf:" + printf ' %s\n' "${post_add_paths[@]}" + + echo "Target kernel family: ${target_family} (expect_intel=${expect_intel})" + matched=0 + failed=() + + for kdir in /lib/modules/*/build; do + [[ -d "${kdir}" ]] || continue + kver="${kdir%/build}"; kver="${kver##*/}" + + if [[ "${kver}" == *azure* ]]; then + echo "Skipping ${kver} (GitHub runner kernel)"; continue + fi + if [[ ! "${kver}" =~ ${target_regex} ]]; then + echo "Skipping ${kver} (family mismatch)"; continue + fi + is_intel=0; [[ "${kver}" == *-intel* ]] && is_intel=1 + if (( is_intel != expect_intel )); then + echo "Skipping ${kver} (intel/canonical mismatch)"; continue + fi + + (( matched += 1 )) + echo "=== Testing ${kver} ===" + dkms remove -m "${package_name}" -v "${package_version}" --all >/dev/null 2>&1 || true + + # Neuter POST_ADD during `dkms add` — we run the source-fetch + # script manually below with the correct kernelver. + cp dkms.conf dkms.conf.ci.bak + sed -i 's|^POST_ADD=.*|POST_ADD="/bin/true"|' dkms.conf + if ! dkms add .; then + failed+=("${kver} (add)") + mv dkms.conf.ci.bak dkms.conf + continue + fi + mv dkms.conf.ci.bak dkms.conf + + src="/usr/src/${package_name}-${package_version}" + build="/var/lib/dkms/${package_name}/${package_version}/build" + ( cd "${src}" && env kernelver="${kver}" bash ./script/dkms-kernel-source.sh "${post_add_paths[@]}" ) + mkdir -p "${build}" + cp -a "${src}/." "${build}/" + + echo "running: dkms build -k ${kver}" + dkms build -m "${package_name}" -v "${package_version}" -k "${kver}" || failed+=("${kver}") + done + + if (( matched == 0 )); then + echo "#### No installed kernel headers matched target family ${target_family}" + exit 1 + fi + if (( ${#failed[@]} > 0 )); then + echo "#### Failed kernels: ${failed[*]}" + exit 1 + fi + echo "#### All builds succeeded for target family ${target_family}" \ No newline at end of file diff --git a/script/dkms-kernel-source.sh b/script/dkms-kernel-source.sh index c3998828..f9259ff8 100755 --- a/script/dkms-kernel-source.sh +++ b/script/dkms-kernel-source.sh @@ -156,16 +156,60 @@ if [[ -z "$archive" ]]; then fi fi -# Read the first archive entry to determine the top-level directory name. -# tar may receive SIGPIPE because we close the stream early; suppress that noise. -IFS=/ read -r archive_root _ < <(tar -tf "$archive" 2>/dev/null || true) +# Cache the archive listing once. Decompressing a ~150MB xz kernel tarball is +# expensive (several seconds), and we consult the listing both to determine +# the top-level directory and to enumerate matching members for every arg. +mapfile -t archive_members < <(tar -tf "$archive" 2>/dev/null || true) +if [[ ${#archive_members[@]} -eq 0 ]]; then + echo "dkms-kernel-source.sh: failed to list contents of ${archive}" >&2 + exit 1 +fi + +# The first entry gives us the top-level directory name. +IFS=/ read -r archive_root _ <<<"${archive_members[0]}" if [[ -z "${archive_root:-}" ]]; then echo "dkms-kernel-source.sh: could not determine archive root directory from ${archive}" >&2 exit 1 fi for arg in "$@"; do - echo "Extracting: $archive_root/$arg" + prefix="$archive_root/$arg" + echo "Extracting: $prefix" + + # Some archives do not contain explicit directory entries. When POST_ADD + # passes a directory, expand members below that prefix. Avoid passing both + # the directory entry and its children to tar, which can trigger false + # "Not found in archive" errors on compressed one-pass reads. + if ! members_output="$( + printf '%s\n' "${archive_members[@]}" | awk -v p="$prefix" ' + { + if (index($0, p "/") == 1 && $0 != p "/") { + print $0 + found_descendants = 1 + } else if ($0 == p || $0 == p "/") { + exact = $0 + } + } + END { + if (!found_descendants && exact != "") { + print exact + } + } + ' + )"; then + echo "dkms-kernel-source.sh: failed to enumerate archive members for '$prefix'" >&2 + exit 1 + fi + + members=() + [[ -n "$members_output" ]] && mapfile -t members <<<"$members_output" + + if [[ ${#members[@]} -eq 0 ]]; then + echo "dkms-kernel-source.sh: no archive members matched '$prefix'" >&2 + exit 1 + fi + tar -xvf "$archive" \ --xform="s,^${archive_root//./\\.}/,$major.$minor.0/," \ - "$archive_root/$arg" + -- \ + "${members[@]}" done From bd4dde44c567fefe2c5a2949b1aa97810ad5e18e Mon Sep 17 00:00:00 2001 From: "Khai Wen, Ng" Date: Mon, 3 Aug 2026 13:25:20 +0800 Subject: [PATCH 5/5] Update .github/workflows/dkms-build.yml Use Ubuntu-latest for external repo. Signed-off-by: Sodhi, Vunny Signed-off-by: Khai Wen, Ng --- .github/workflows/dkms-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dkms-build.yml b/.github/workflows/dkms-build.yml index a345906e..9b6a4f33 100644 --- a/.github/workflows/dkms-build.yml +++ b/.github/workflows/dkms-build.yml @@ -14,7 +14,7 @@ defaults: jobs: ubuntu-dkms: name: Ubuntu ${{ matrix.os_version }} DKMS (${{ matrix.kernel_family }}) - runs-on: [self-hosted, linux] + runs-on: ubuntu-latest container: ubuntu:${{ matrix.os_version }} env: DEBIAN_FRONTEND: noninteractive