diff --git a/.github/workflows/dkms-build.yml b/.github/workflows/dkms-build.yml new file mode 100644 index 0000000..9b6a4f3 --- /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: ubuntu-latest + 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/drivers/media/i2c/maxim-serdes/max96717.c b/drivers/media/i2c/maxim-serdes/max96717.c index a8733e8..9fc72fd 100644 --- a/drivers/media/i2c/maxim-serdes/max96717.c +++ b/drivers/media/i2c/maxim-serdes/max96717.c @@ -1810,6 +1810,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 +1898,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 625c795..13791b5 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 017950c..cfd8f16 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; /* @@ -1760,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--) { @@ -1785,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) { @@ -2528,7 +2551,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 +2697,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; @@ -3473,5 +3498,91 @@ 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; + int ret; + + 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); + } + + 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"); + +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) { + dev_err(priv->dev, + "resume: failed to enable POC supplies: %d\n", 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; + } + + /* Resume: re-negotiate serializer addresses and restore link mask. */ + if (des->ops->select_links) { + for (i = 0; i < des->ops->num_links; 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) { + 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: + 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 c495898..a2d760e 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 4b05a56..8b1f226 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) @@ -2232,6 +2268,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); + } + + for (i = 0; i < ser->ops->num_pipes; i++) { + struct max_ser_pipe *pipe = &ser->pipes[i]; + + if (ser->ops->set_pipe_enable && pipe->enabled) + ser->ops->set_pipe_enable(ser, pipe, 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; + } + + 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; +} +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 eb5243f..b57776e 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); diff --git a/script/dkms-kernel-source.sh b/script/dkms-kernel-source.sh index c399882..f9259ff 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