linux-postmarketos-qcom-sm8550: motorola-rtwo display, DP, WiFi, audio DSPs

Bring-up of the Moto Edge 40 Pro (rtwo) on the SM8550 kernel:

- dts: NT37705 DSI panel with DSC, Goodix GT9916P touch, ADSP/CDSP with
  pmic-glink (battery, UCSI), Adreno 740, WiFi (WCN7850/ath12k), physical
  keys, and USB-C DisplayPort (pmic-glink altmode -> PS5169 redriver ->
  FSA4480 -> QMP combo PHY -> mdss_dp0) following the sm8550-qrd layout.
- DP link capped at HBR (link-frequencies on &mdss_dp0_out): the DP->HDMI
  converter of a common USB-C hub trains fine at HBR2 but its HDMI output
  flickers and drops HPD; HBR x2 still covers 1080p60.
- new drivers: typec-mux-ps5169.c (Parade PS5169 redriver as typec
  switch/retimer), panel-novatek-nt37705.c.
- patches: msm DP invalid LTTPR caps + no push_idle with the link down
  (hardware reset on unplug), msm DSI keeps the panel DSC config, ath12k
  regd fixes and MHI keep-awake, ICC keepalive for the MDP fetch path.
- rtwo.config / config-motorola-rtwo.aarch64: options for all of the above.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnHwDU5yL8L6qgPEN5CV5m
This commit is contained in:
2026-09-26 20:02:55 -03:00
co-authored by Claude Opus 5.5
parent f3fac3a449
commit 8a87204d48
14 changed files with 2638 additions and 64 deletions
@@ -5,7 +5,7 @@ maintainer="snlavaise <snlavaise@gmail.com>"
_flavor="postmarketos-qcom-sm8550" _flavor="postmarketos-qcom-sm8550"
pkgname=linux-$_flavor pkgname=linux-$_flavor
pkgver=7.2 pkgver=7.2
pkgrel=13 pkgrel=48
pkgdesc="Mainline kernel for Qualcomm SM8550 (Snapdragon 8 Gen 2) + Motorola rtwo" pkgdesc="Mainline kernel for Qualcomm SM8550 (Snapdragon 8 Gen 2) + Motorola rtwo"
arch="aarch64" arch="aarch64"
_carch="arm64" _carch="arm64"
@@ -47,6 +47,16 @@ source="
config-motorola-rtwo.aarch64 config-motorola-rtwo.aarch64
rtwo.config rtwo.config
sm8550-motorola-rtwo.dts sm8550-motorola-rtwo.dts
ath12k-fix-regd-update-completion-hang.patch
ath12k-fix-regd-range-wideband.patch
icc-sm8550-keepalive-mm0-mdp-fetch.patch
ath12k-htc-log-every-rx-header.patch
ath12k-mhi-keep-awake.patch
msm-dsi-keep-panel-dsc-config.patch
msm-dp-ignore-invalid-lttpr-caps.patch
msm-dp-no-push-idle-when-link-down.patch
panel-novatek-nt37705.c
typec-mux-ps5169.c
" "
builddir="$srcdir/linux-${pkgver/_/-}" builddir="$srcdir/linux-${pkgver/_/-}"
@@ -60,6 +70,49 @@ prepare() {
sed -i '/sm8550-mtp\.dtb/a dtb-$(CONFIG_ARCH_QCOM) += sm8550-motorola-rtwo.dtb' \ sed -i '/sm8550-mtp\.dtb/a dtb-$(CONFIG_ARCH_QCOM) += sm8550-motorola-rtwo.dtb' \
"$_dtsdir/Makefile" "$_dtsdir/Makefile"
fi fi
# Not-yet-upstream panel driver (DSI command mode + DSC, Motorola rtwo).
cp "$srcdir/panel-novatek-nt37705.c" \
drivers/gpu/drm/panel/panel-novatek-nt37705.c
if ! grep -q "CONFIG_DRM_PANEL_NOVATEK_NT37705" drivers/gpu/drm/panel/Kconfig; then
cat >> drivers/gpu/drm/panel/Kconfig <<'EOK'
config DRM_PANEL_NOVATEK_NT37705
tristate "Novatek NT37705 DSI panel"
depends on OF
depends on DRM_MIPI_DSI
depends on BACKLIGHT_CLASS_DEVICE
select DRM_DISPLAY_DSC_HELPER
select DRM_DISPLAY_HELPER
help
Say Y here if you want to enable support for Novatek NT37705 DSI
panel module. 1080x2400 command mode DSC panel from CSOT, used on
the Motorola rtwo.
EOK
fi
if ! grep -q "panel-novatek-nt37705" drivers/gpu/drm/panel/Makefile; then
echo 'obj-$(CONFIG_DRM_PANEL_NOVATEK_NT37705) += panel-novatek-nt37705.o' \
>> drivers/gpu/drm/panel/Makefile
fi
# Not-yet-upstream Parade PS5169 USB-C redriver (DisplayPort alt mode).
cp "$srcdir/typec-mux-ps5169.c" drivers/usb/typec/mux/ps5169.c
if ! grep -q "CONFIG_TYPEC_MUX_PS5169" drivers/usb/typec/mux/Kconfig; then
cat >> drivers/usb/typec/mux/Kconfig <<'EOK'
config TYPEC_MUX_PS5169
tristate "Parade PS5169 Type-C redriver driver"
depends on I2C
depends on DRM || DRM=n
select DRM_AUX_BRIDGE if DRM_BRIDGE && OF
select REGMAP_I2C
help
Say Y or M if your system has a Parade PS5169 USB/DP Type-C
redriver, as found on the Motorola rtwo.
EOK
fi
if ! grep -q "ps5169" drivers/usb/typec/mux/Makefile; then
echo 'obj-$(CONFIG_TYPEC_MUX_PS5169) += ps5169.o' \
>> drivers/usb/typec/mux/Makefile
fi
# Build this DTB with -@ so it gets a /__symbols__ node. The Motorola # Build this DTB with -@ so it gets a /__symbols__ node. The Motorola
# SM8550 ABL runs its dtbo-apply path (libufdt) against the base DTB # SM8550 ABL runs its dtbo-apply path (libufdt) against the base DTB
# and drops to the bootloader menu if the symbols are missing. # and drops to the bootloader menu if the symbols are missing.
@@ -110,7 +163,17 @@ package() {
# (same tarball as linux-postmarketos-mainline, ~150 MB download). # (same tarball as linux-postmarketos-mainline, ~150 MB download).
sha512sums=" sha512sums="
52d5aa7f94f6aa6141debf4ceb4fc7a2e75505a7cd38b8854e501be83c2b57863020f463f2fdb2fc93667e6cfe13274d077e5207a611e4797bd9404ee0e64c58 linux-postmarketos-qcom-sm8550-7.2.tar.gz 52d5aa7f94f6aa6141debf4ceb4fc7a2e75505a7cd38b8854e501be83c2b57863020f463f2fdb2fc93667e6cfe13274d077e5207a611e4797bd9404ee0e64c58 linux-postmarketos-qcom-sm8550-7.2.tar.gz
f9ae52489f6b9e6fef64952943cd09732a5bef90a0e8765db03c256483028368687d6d5a1d80dd813a513e7e80d91f8b47f49b91cff67266b76f1d6275a7fea0 config-motorola-rtwo.aarch64 0ab844d874289be5529c7f83a1bdd16d867ea6d5f3f873556779cbd27908316fd5076c201d6b6fee40991da648eadc982422cc0136fe2c9034b7b4846e08a028 config-motorola-rtwo.aarch64
53441c23146d76bb0eaa358a25f6cce4f21740cfb65b755df52bbb218daf2cda30b3d80fa6ca4e283ea1afe2b0fa708307b678b421898100710b8fb99b08d6c5 rtwo.config a03843765d996a4555f991b277dd9cc35f1c96da1b740330a3497202d355ec95406f380a0ec6a778d638bf9acfc1f28dbc441e156fb75b1cf00720af136f69b8 rtwo.config
7f51d30b9e2d7fb781fbf066a8f8696816e881686c9a287b20d5d213955912c783e1534479c9bd9701cd29c1e1769132653a56f7b008eb7368083e5f523fd731 sm8550-motorola-rtwo.dts ff2da1726b87210497d3d3961af5d95b9dfb160d48af90bb604dbb321b89c50689a06722c50db265aaf5c95b0078da278939efda8e1a8f30232827d27da1c8ca sm8550-motorola-rtwo.dts
a068e35a031858161e7a5ac00d7d2db9fd0d842fc19d3ebf08b03275ef470eff5462921e36390f535e2556619b6911192b6e2ccf7c313edafaf22d6da44c1d8e ath12k-fix-regd-update-completion-hang.patch
891b9b15e9113488667f2dcf9f1502aab24291228cc4096f358acbf4b67d299fb90b2d00b4e103139cd2160339a10833cddb781838d1f03721fef07ea45b3213 ath12k-fix-regd-range-wideband.patch
acbe599c08877065ea4419363da7ab75a16a557aad4b13161f22270f03b27efac1be487884896900c3e80d6df8e3545ca6b1df940d24badef1825fe0d27147ac icc-sm8550-keepalive-mm0-mdp-fetch.patch
cf4889c4dca8d3bac7544d71284e6b592d6014d9ad94badfc6d2cbe56a8402c61e4c0811c2f48d3352b446de9fbcf36b58226431c9276956d7ff3fe08c9eb589 ath12k-htc-log-every-rx-header.patch
6e01370d013278faba203e57ba20700d500639136672c040c0924a600fc3dcd8185a9f558ee7bf8a9ad5412113f6b6ab1901423006bb839e808dbcb151a16383 ath12k-mhi-keep-awake.patch
7673bec5661e692721bce41e72a330180e653d2b1b0a5cb3d36bdc869ad4a32ce23a6048d01c9cf2cf5c78c2a7e33899a0a149c65e9754d9d1c2cd71b542b84c msm-dsi-keep-panel-dsc-config.patch
3268835bcb539a77d16c888d933c856df6bb591861d30c7c61c6e0ab70751bac6248961ec30fedf9a62bebdcd63113cf1dd14e58a1b53262493d5e0e90dbbe3f msm-dp-ignore-invalid-lttpr-caps.patch
4f5ea9bfe01a6df166fdad601f6bd74d4b39049be924e8b4df58a8acbba7e9ebfc568bd81df70354209642f68e113804b9d1b62cbd3c83c319fae5fefcf07595 msm-dp-no-push-idle-when-link-down.patch
e8df16de9b413fedd0d36414311216e68e69363e1bfbba8c7b892120f860d217b58ceeffeb92566a5617177b0f190a64e9e2284d6d4285e72db35cf40770e41a panel-novatek-nt37705.c
dd95e80f62879d5167e7bfc7d74b4a64669a43dbf5aacb6a13e62d1ed4a88677ef1042f8cecc46e05e3d12ba75f070a5927df308c90fe846af93b78c6b310e80 typec-mux-ps5169.c
" "
@@ -0,0 +1,41 @@
Fix regulatory range for wideband radios (carried by OpenWrt/Quectel
trees for WCN7850 and wideband QCN9274; NOT in v7.2 mainline).
ath12k_regd_update() assumes that a radio advertising 6 GHz cannot also
operate on 5 GHz. For the rtwo the WCN7850 is a single SMM radio whose
5 GHz band spans [4900-7125] (hw_mode 5, 2.4+5+6 GHz in one pdev), so
this old check picks only the 6 GHz portion of the band and drops the
real 5 GHz half from ar->freq_range. ath12k_mac_update_freq_range()
unions multiple calls, so the fixed conditions do not overwrite -- they
extend the advertised range to cover both portions.
Diagnostic build 2026-09-22 (same experiment as the wcn7850-pmu pinctrl
DT change): determines whether the cropped regulatory range sent to the
WCN7850 WMI path is what stalls the firmware at ath12k_mac_start()
("wmi command 16387 timeout", -EAGAIN on insufficient HTC credits after
the WMI channel list).
Blame/author: Robert Marko <robert.marko@sartura.hr>
From: https://git-03.infra.openwrt.org/openwrt/staging/stintel/plain/package/kernel/mac80211/patches/ath12k/104-wifi-ath12k-fix-regulatory-range-for-wideband-radios.patch
--- a/drivers/net/wireless/ath/ath12k/reg.c
+++ b/drivers/net/wireless/ath/ath12k/reg.c
@@ -310,7 +310,8 @@ int ath12k_regd_update(struct ath12k *ar, bool init)
ath12k_mac_update_freq_range(ar, freq_low, freq_high);
}
- if (supported_bands & WMI_HOST_WLAN_5GHZ_CAP && !ar->supports_6ghz) {
+ if (supported_bands & WMI_HOST_WLAN_5GHZ_CAP &&
+ reg_cap->low_5ghz_chan < ATH12K_MIN_6GHZ_FREQ) {
if (ab->hw_params->single_pdev_only) {
phy_id = ar->pdev->cap.band[WMI_HOST_WLAN_5GHZ_CAP].phy_id;
reg_cap = &ab->hal_reg_cap[phy_id];
@@ -322,7 +323,8 @@ int ath12k_regd_update(struct ath12k *ar, bool init)
ath12k_mac_update_freq_range(ar, freq_low, freq_high);
}
- if (supported_bands & WMI_HOST_WLAN_5GHZ_CAP && ar->supports_6ghz) {
+ if (supported_bands & WMI_HOST_WLAN_5GHZ_CAP &&
+ reg_cap->high_5ghz_chan >= ATH12K_MIN_6GHZ_FREQ) {
freq_low = max(reg_cap->low_5ghz_chan, ab->reg_freq_6ghz.start_freq);
freq_high = min(reg_cap->high_5ghz_chan, ab->reg_freq_6ghz.end_freq);
@@ -0,0 +1,26 @@
--- a/drivers/net/wireless/ath/ath12k/wmi.c 2026-09-20 02:48:13.191255011 -0300
+++ b/drivers/net/wireless/ath/ath12k/wmi.c 2026-09-20 02:48:13.192653745 -0300
@@ -6939,6 +6939,15 @@
goto mem_free;
}
+ /* Assign pdev_idx as soon as we have a valid reg_info, BEFORE the
+ * FALLBACK/DROP paths below, so ar->regd_update_completed still gets
+ * signalled even when the event is dropped because the country code
+ * already matches the default regd (fixes a permanent hang of any
+ * caller waiting on regd_update_completed, e.g. WMI_PDEV_SET_PARAM
+ * during mac80211 hw_register on bring-up).
+ */
+ pdev_idx = reg_info->phy_id;
+
ret = ath12k_reg_validate_reg_info(ab, reg_info);
if (ret == ATH12K_REG_STATUS_FALLBACK) {
ath12k_warn(ab, "failed to validate reg info %d\n", ret);
@@ -6955,7 +6964,6 @@
}
/* free old reg_info if it exist */
- pdev_idx = reg_info->phy_id;
if (ab->reg_info[pdev_idx]) {
ath12k_reg_reset_reg_info(ab->reg_info[pdev_idx]);
kfree(ab->reg_info[pdev_idx]);
@@ -0,0 +1,39 @@
Log the raw HTC header of every received message.
Diagnostic-only, for the rtwo WiFi bring-up. ath12k_mac_start() dies on
the second WMI command with -EAGAIN out of ath12k_htc_send()'s
"insufficient credits" path, and the endpoint's credits are only ever
replenished by an ATH12K_HTC_RECORD_CREDITS record carried in the
*trailer* of a received HTC message -- there is no dedicated credit
event. /proc/interrupts shows ce2 (target->host WMI) taking real
interrupts, so messages do arrive; what cannot be seen from outside is
whether any of them ever carries a trailer at all.
The existing "htc rx completion ep %d" debug line does not answer that:
it is printed at the very end of the handler, so it misses both the EP0
control traffic and the "zero length packet with trailer data, just drop
these" early return -- which is exactly the shape a pure credit-report
message has. Print the decoded header right where the fields are read
instead, before anything can return early.
Sits under the existing ATH12K_DBG_HTC (0x4) mask, so it costs one
branch per RX when the mask is unset and disappears entirely without
CONFIG_ATH12K_DEBUG. Drop this patch once WiFi works.
--- a/drivers/net/wireless/ath/ath12k/htc.c
+++ b/drivers/net/wireless/ath/ath12k/htc.c
@@ -292,6 +292,14 @@
trailer_present = le32_get_bits(hdr->htc_info, HTC_HDR_FLAGS) &
ATH12K_HTC_FLAG_TRAILER_PRESENT;
+ ath12k_dbg(ab, ATH12K_DBG_HTC,
+ "htc rx hdr eid %d payload_len %d flags 0x%02x trailer %d trailer_len %d\n",
+ eid, payload_len,
+ (u8)le32_get_bits(hdr->htc_info, HTC_HDR_FLAGS),
+ trailer_present ? 1 : 0,
+ trailer_present ?
+ (int)le32_get_bits(hdr->ctrl_info, HTC_HDR_CONTROLBYTES0) : 0);
+
if (trailer_present) {
u8 *trailer;
@@ -0,0 +1,87 @@
Add ath12k.mhi_keep_awake to keep the WCN7850 out of MHI M1/M2.
Diagnostic for the rtwo WiFi bring-up. With CONFIG_ATH12K_DEBUG the log
shows that the firmware returns HTC credits for the WMI endpoint
normally (trailers on EP0, ~3 ms after each command) until the moment
the device reports M1 and the host moves it to M2 (MHI_CB_IDLE, 22 us
after the last credit is spent). After that the target sends nothing on
any endpoint and there is no RDDM/SYS_ERR, so it is asleep, not crashed.
WCN7850 uses shadow registers for the CE doorbells (below
ACCESS_ALWAYS_OFF), so those writes never go through pci_ops->wakeup().
mhi_keep_awake=1 takes an MHI device wake reference right after power
up and holds it until mhi_stop, so the device stays in M0. With it
off, MHI_CB_IDLE is logged at warn level to see when it happens.
--- a/drivers/net/wireless/ath/ath12k/pci.h 2026-09-22 20:54:12.225709859 -0300
+++ b/drivers/net/wireless/ath/ath12k/pci.h 2026-09-22 20:54:12.243985679 -0300
@@ -93,6 +93,7 @@
ATH12K_PCI_FLAG_IS_MSI_64,
ATH12K_PCI_ASPM_RESTORE,
ATH12K_PCI_FLAG_MULTI_MSI_VECTORS,
+ ATH12K_PCI_FLAG_MHI_KEEP_AWAKE,
};
struct ath12k_pci_ops {
--- a/drivers/net/wireless/ath/ath12k/mhi.c 2026-09-22 20:54:12.223301456 -0300
+++ b/drivers/net/wireless/ath/ath12k/mhi.c 2026-09-22 20:54:12.244083641 -0300
@@ -18,6 +18,16 @@
#define OTP_VALID_DUALMAC_BOARD_ID_MASK 0x1000
#define MHI_CB_INVALID 0xff
+/* rtwo bring-up: the WMI endpoint runs out of HTC credits at the exact
+ * moment the device reports M1 and the host moves it to M2
+ * (MHI_CB_IDLE); after that the target never sends anything again.
+ * Holding MHI device wake for the whole session keeps it in M0, to
+ * tell whether the M1/M2 transition is what loses the WMI doorbells.
+ */
+static bool ath12k_mhi_keep_awake;
+module_param_named(mhi_keep_awake, ath12k_mhi_keep_awake, bool, 0644);
+MODULE_PARM_DESC(mhi_keep_awake, "Hold MHI device wake while powered on (never enter M1/M2)");
+
void ath12k_mhi_set_mhictrl_reset(struct ath12k_base *ab)
{
u32 val;
@@ -148,6 +158,11 @@
case MHI_CB_SYS_ERROR:
ath12k_warn(ab, "firmware crashed: MHI_CB_SYS_ERROR\n");
break;
+ case MHI_CB_IDLE:
+ /* rtwo: log at warn level so it shows up without debug_mask */
+ ath12k_warn(ab, "mhi idle: device moved to M2 (keep_awake %d)\n",
+ test_bit(ATH12K_PCI_FLAG_MHI_KEEP_AWAKE, &ab_pci->flags));
+ break;
case MHI_CB_EE_RDDM:
if (ab_pci->mhi_pre_cb == MHI_CB_EE_RDDM) {
ath12k_dbg(ab, ATH12K_DBG_BOOT,
@@ -493,6 +508,20 @@
if (ret)
goto out;
+ if (ath12k_mhi_keep_awake) {
+ ret = mhi_device_get_sync(ab_pci->mhi_ctrl->mhi_dev);
+ if (ret) {
+ ath12k_warn(ab_pci->ab,
+ "mhi_keep_awake: failed to hold device wake: %d\n",
+ ret);
+ ret = 0;
+ } else {
+ set_bit(ATH12K_PCI_FLAG_MHI_KEEP_AWAKE, &ab_pci->flags);
+ ath12k_info(ab_pci->ab,
+ "mhi_keep_awake: holding device wake, M1/M2 disabled\n");
+ }
+ }
+
return 0;
out:
@@ -501,6 +530,9 @@
void ath12k_mhi_stop(struct ath12k_pci *ab_pci, bool is_suspend)
{
+ if (test_and_clear_bit(ATH12K_PCI_FLAG_MHI_KEEP_AWAKE, &ab_pci->flags))
+ mhi_device_put(ab_pci->mhi_ctrl->mhi_dev);
+
/* During suspend we need to use mhi_power_down_keep_dev()
* workaround, otherwise ath12k_core_resume() will timeout
* during resume.
@@ -5464,7 +5464,7 @@ CONFIG_BCM7038_WDT=m
# #
# CONFIG_USBPCWATCHDOG is not set # CONFIG_USBPCWATCHDOG is not set
# CONFIG_KEEMBAY_WATCHDOG is not set # CONFIG_KEEMBAY_WATCHDOG is not set
# CONFIG_GUNYAH_WATCHDOG is not set CONFIG_GUNYAH_WATCHDOG=y
CONFIG_SSB_POSSIBLE=y CONFIG_SSB_POSSIBLE=y
# CONFIG_SSB is not set # CONFIG_SSB is not set
CONFIG_BCMA_POSSIBLE=y CONFIG_BCMA_POSSIBLE=y
@@ -11732,10 +11732,10 @@ CONFIG_SQUASHFS_FRAGMENT_CACHE_SIZE=3
CONFIG_PSTORE=y CONFIG_PSTORE=y
CONFIG_PSTORE_DEFAULT_KMSG_BYTES=10240 CONFIG_PSTORE_DEFAULT_KMSG_BYTES=10240
CONFIG_PSTORE_COMPRESS=y CONFIG_PSTORE_COMPRESS=y
# CONFIG_PSTORE_CONSOLE is not set CONFIG_PSTORE_CONSOLE=y
# CONFIG_PSTORE_PMSG is not set # CONFIG_PSTORE_PMSG is not set
# CONFIG_PSTORE_FTRACE is not set # CONFIG_PSTORE_FTRACE is not set
CONFIG_PSTORE_RAM=m CONFIG_PSTORE_RAM=y
# CONFIG_PSTORE_BLK is not set # CONFIG_PSTORE_BLK is not set
# CONFIG_UFS_FS is not set # CONFIG_UFS_FS is not set
CONFIG_EROFS_FS=m CONFIG_EROFS_FS=m
@@ -0,0 +1,25 @@
--- a/drivers/interconnect/qcom/sm8550.c
+++ b/drivers/interconnect/qcom/sm8550.c
@@ -1136,6 +1136,22 @@ static struct qcom_icc_bcm bcm_mc0 = {
static struct qcom_icc_bcm bcm_mm0 = {
.name = "MM0",
+ /*
+ * qns_mem_noc_hf is the only node this BCM covers, and it's what
+ * qnm_mdp (the MDP's own interconnect master) feeds into. Our
+ * bring-up has no MDSS/DPU driver, so nothing in Linux ever calls
+ * icc_set_bw() on this path -- but the DPU hardware is still live,
+ * inherited from U-Boot, and keeps fetching the framebuffer over
+ * this exact bus. Without keepalive, icc_sync_state() (core.c,
+ * gated on every icc provider probing -- in practice on pcie0/
+ * WiFi finishing last) zeroes the firmware's boot-time vote on
+ * this BCM and the display freezes mid-boot, at a random-looking
+ * point that always lines up with pcie0 finishing probe. MC0/SH0/
+ * CN0/SN0/QUP0-2 already get this same treatment for the same
+ * reason (DDR/LLCC/config/system NoC/QUP buses that must never
+ * collapse to zero just because no driver claimed them yet).
+ */
+ .keepalive = true,
.num_nodes = 1,
.nodes = { &qns_mem_noc_hf },
};
@@ -0,0 +1,54 @@
--- a/drivers/gpu/drm/msm/dp/dp_display.c
+++ b/drivers/gpu/drm/msm/dp/dp_display.c
@@ -251,16 +251,34 @@
int rc, lttpr_count;
if (drm_dp_read_lttpr_common_caps(dp->aux, dpcd, dp->link->lttpr_common_caps))
- return 0;
+ goto no_lttpr;
+
+ /*
+ * Some sinks without LTTPR support (e.g. DP->HDMI converters) mirror
+ * their receiver caps at 0xf0000 instead of NAKing the read. Only
+ * trust the block if it carries a valid LTTPR structure revision and
+ * repeater count: otherwise the caps would clamp the link to whatever
+ * lands in the "max lane count"/"max link rate" slots, and writing
+ * PHY_REPEATER_MODE would end up in the sink's own DPCD.
+ */
+ if (dp->link->lttpr_common_caps[0] < 0x14)
+ goto no_lttpr;
lttpr_count = drm_dp_lttpr_count(dp->link->lttpr_common_caps);
+ if (lttpr_count < 0)
+ goto no_lttpr;
+
rc = drm_dp_lttpr_init(dp->aux, lttpr_count);
if (rc) {
DRM_ERROR("failed to set LTTPRs transparency mode, rc=%d\n", rc);
- return 0;
+ goto no_lttpr;
}
return lttpr_count;
+
+no_lttpr:
+ memset(dp->link->lttpr_common_caps, 0, sizeof(dp->link->lttpr_common_caps));
+ return 0;
}
static int msm_dp_display_process_hpd_high(struct msm_dp_display_private *dp)
--- a/drivers/gpu/drm/msm/dp/dp_panel.c
+++ b/drivers/gpu/drm/msm/dp/dp_panel.c
@@ -101,6 +101,12 @@
msm_dp_panel->vsc_sdp_supported = drm_dp_vsc_sdp_supported(panel->aux, dpcd);
link_info = &msm_dp_panel->link_info;
+ /*
+ * Everything below is derived from the current sink. Start from
+ * scratch so a rate from a previous sink (the fallback path only runs
+ * when rate is 0) does not stick across unplug/replug.
+ */
+ memset(link_info, 0, sizeof(*link_info));
link_info->revision = dpcd[DP_DPCD_REV];
major = (link_info->revision >> 4) & 0x0f;
minor = link_info->revision & 0x0f;
@@ -0,0 +1,18 @@
--- a/drivers/gpu/drm/msm/dp/dp_display.c
+++ b/drivers/gpu/drm/msm/dp/dp_display.c
@@ -1434,6 +1434,15 @@
msm_dp_display = container_of(dp, struct msm_dp_display_private, msm_dp_display);
+ /*
+ * If link training failed in atomic_enable, the mainlink was already
+ * deinitialized (link clocks and PHY off) and the stream never came
+ * up. Writing DP_STATE_CTRL then hits an unclocked block, which hangs
+ * the bus and resets the SoC on unplug or the next modeset.
+ */
+ if (!dp->power_on)
+ return;
+
msm_dp_ctrl_push_idle(msm_dp_display->ctrl);
}
@@ -0,0 +1,18 @@
--- a/drivers/gpu/drm/msm/dsi/dsi_host.c
+++ b/drivers/gpu/drm/msm/dsi/dsi_host.c
@@ -1870,6 +1870,15 @@
{
int ret;
+ /*
+ * Some panels (e.g. Novatek DDICs with fixed rate control tables in
+ * the decoder) need the exact RC configuration the vendor encoder
+ * used, not the one derived here from the generic DSC tables. Keep a
+ * configuration the panel driver already filled in completely.
+ */
+ if (dsc->rc_model_size && dsc->slice_chunk_size && dsc->initial_dec_delay)
+ return 0;
+
if (dsc->bits_per_pixel & 0xf) {
DRM_DEV_ERROR(&msm_host->pdev->dev, "DSI does not support fractional bits_per_pixel\n");
return -EINVAL;
@@ -0,0 +1,594 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (c) 2026 snlavaise <snlavaise@gmail.com>
*
* DRM panel driver for the CSOT NT37705 (Novatek NT37705) command mode DSC
* panel used on the Motorola rtwo (Moto Edge 40 Pro, SM8550).
*
* Sequences transcribed byte-by-byte from the vendor device tree node
* "qcom,mdss_dsi_mot_csot_nt37705_667_curve_1080x2400_dsc_cmd_v1"
* (panel name mipi_mot_cmd_csot_1080p_dsc_667, panel supplier "csot"),
* see pmos/panel-nt37705-v1-dump.md. Only the 60 Hz timing (timing@0, 2f01 +
* e000) is exposed for the first bring-up. The other rates only differ in
* their timing-switch-command: 120 Hz 2f02/e041, 90 Hz 2f04/e000,
* 48 Hz 2f05/e000, 165 Hz 2f03/e041 (same porches and DSI clock).
*
* DSC: besides the Novatek MCS setup in the on command (0x90 enable, 0x91
* parameters), the standard PPS and compression-mode packets are sent after
* it, same as the mainline NT37801 driver (same Novatek flow) and the
* downstream driver, which sends the PPS by itself.
*/
#include <linux/backlight.h>
#include <linux/delay.h>
#include <linux/gpio/consumer.h>
#include <linux/ktime.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/regulator/consumer.h>
#include <video/mipi_display.h>
#include <drm/display/drm_dsc.h>
#include <drm/display/drm_dsc_helper.h>
#include <drm/drm_mipi_dsi.h>
#include <drm/drm_modes.h>
#include <drm/drm_panel.h>
#include <drm/drm_probe_helper.h>
struct nt37705_csot {
struct drm_panel panel;
struct mipi_dsi_device *dsi;
struct gpio_desc *reset_gpio;
struct regulator_bulk_data *supplies;
ktime_t off_since;
};
static const struct regulator_bulk_data nt37705_csot_supplies[] = {
{ .supply = "vddio" },
{ .supply = "vdd" },
{ .supply = "vci" },
};
static inline struct nt37705_csot *to_nt37705_csot(struct drm_panel *panel)
{
return container_of(panel, struct nt37705_csot, panel);
}
/*
* Vendor reset-sequence: <0 12 1 1 0 1 1 13> (value -> duration ms).
* Those are raw line levels (downstream uses gpio_set_value()), while
* reset-gpios is GPIO_ACTIVE_LOW here, so the logical values are inverted:
* assert 12 ms, release 1 ms, assert 1 ms, release 13 ms (ends released).
*/
static void nt37705_csot_reset(struct nt37705_csot *ctx)
{
gpiod_set_value_cansleep(ctx->reset_gpio, 1);
usleep_range(12000, 13000);
gpiod_set_value_cansleep(ctx->reset_gpio, 0);
usleep_range(1000, 2000);
gpiod_set_value_cansleep(ctx->reset_gpio, 1);
usleep_range(1000, 2000);
gpiod_set_value_cansleep(ctx->reset_gpio, 0);
usleep_range(13000, 14000);
}
/*
* Rate control for DSC 1.1, 10 bpc, 8 bpp as the Motorola/Qualcomm downstream
* driver sets it for panels flagged qcom,mdss-dsc-novateck-ic
* (sde_dsc_populate_dsc_config_nt(), DSC_V11_10BPC_8BPP entry of the *_nt
* tables). The DDIC decoder uses these fixed tables, which differ from the
* generic VESA/drm ones in 8 of the 15 ranges, so the host encoder must use
* them too; msm keeps a configuration filled in this completely (see
* msm-dsi-keep-panel-dsc-config.patch). Derived values reproduce the stock
* 0x91 bytes exactly.
*/
static const u8 nt37705_rc_min_qp[DSC_NUM_BUF_RANGES] = {
0, 4, 5, 5, 7, 7, 7, 7, 7, 8, 9, 9, 9, 12, 16
};
static const u8 nt37705_rc_max_qp[DSC_NUM_BUF_RANGES] = {
8, 8, 9, 10, 11, 11, 11, 12, 13, 14, 14, 15, 15, 16, 17
};
static const s8 nt37705_rc_bpg[DSC_NUM_BUF_RANGES] = {
2, 0, 0, -2, -4, -6, -8, -8, -8, -10, -10, -10, -12, -12, -12
};
static const u8 nt37705_rc_buf_thresh[DSC_NUM_BUF_RANGES - 1] = {
0x0e, 0x1c, 0x2a, 0x38, 0x46, 0x54, 0x62,
0x69, 0x70, 0x77, 0x79, 0x7b, 0x7d, 0x7e
};
static void nt37705_csot_dsc_config_nt(struct drm_dsc_config *dsc)
{
int bpp = dsc->bits_per_pixel >> 4;
int bpc = dsc->bits_per_component;
int groups_per_line, groups_total, slice_bits;
int pre_mux_bits, mux_bits, final_scale, i;
dsc->rc_model_size = DSC_RC_MODEL_SIZE_CONST;
dsc->first_line_bpg_offset = 13;
dsc->rc_edge_factor = DSC_RC_EDGE_FACTOR_CONST;
dsc->rc_tgt_offset_high = DSC_RC_TGT_OFFSET_HI_CONST;
dsc->rc_tgt_offset_low = DSC_RC_TGT_OFFSET_LO_CONST;
dsc->simple_422 = false;
dsc->convert_rgb = true;
dsc->vbr_enable = false;
for (i = 0; i < DSC_NUM_BUF_RANGES - 1; i++)
dsc->rc_buf_thresh[i] = nt37705_rc_buf_thresh[i];
for (i = 0; i < DSC_NUM_BUF_RANGES; i++) {
dsc->rc_range_params[i].range_min_qp = nt37705_rc_min_qp[i];
dsc->rc_range_params[i].range_max_qp = nt37705_rc_max_qp[i];
dsc->rc_range_params[i].range_bpg_offset =
nt37705_rc_bpg[i] & DSC_RANGE_BPG_OFFSET_MASK;
}
dsc->rc_quant_incr_limit0 = 15;
dsc->rc_quant_incr_limit1 = 15;
dsc->initial_offset = 6144;
dsc->initial_xmit_delay = 512;
dsc->flatness_min_qp = 7;
dsc->flatness_max_qp = 16;
dsc->line_buf_depth = bpc + 1;
dsc->mux_word_size = DSC_MUX_WORD_SIZE_8_10_BPC;
groups_per_line = DIV_ROUND_UP(dsc->slice_width, 3);
dsc->slice_chunk_size = DIV_ROUND_UP(dsc->slice_width * bpp, 8);
dsc->initial_dec_delay = DIV_ROUND_UP(dsc->rc_model_size -
dsc->initial_offset +
dsc->initial_xmit_delay * bpp +
groups_per_line *
dsc->first_line_bpg_offset, bpp) -
dsc->initial_xmit_delay;
dsc->initial_scale_value = 8 * dsc->rc_model_size /
(dsc->rc_model_size - dsc->initial_offset);
slice_bits = 8 * dsc->slice_chunk_size * dsc->slice_height;
groups_total = groups_per_line * dsc->slice_height;
dsc->nfl_bpg_offset = DIV_ROUND_UP(dsc->first_line_bpg_offset * 2048,
dsc->slice_height - 1);
pre_mux_bits = 3 * (dsc->mux_word_size + (4 * bpc + 4) - 2);
mux_bits = pre_mux_bits - (dsc->mux_word_size -
((slice_bits - pre_mux_bits) % dsc->mux_word_size));
dsc->slice_bpg_offset = DIV_ROUND_UP(2048 * (dsc->rc_model_size -
dsc->initial_offset +
mux_bits), groups_total);
dsc->final_offset = dsc->rc_model_size -
dsc->initial_xmit_delay * bpp + mux_bits;
final_scale = 8 * dsc->rc_model_size /
(dsc->rc_model_size - dsc->final_offset);
dsc->scale_increment_interval = (2048 * dsc->final_offset) /
((final_scale - 9) * (dsc->nfl_bpg_offset +
dsc->slice_bpg_offset));
dsc->scale_decrement_interval = groups_per_line /
(dsc->initial_scale_value - 8);
}
/*
* Novatek 0x91: the panel-side DSC decoder parameters. The layout was checked
* field by field against the mainline NT37801 driver, whose hardcoded 0x91
* matches drm_dsc_compute_rc_parameters() exactly.
*
* Built from the config the host actually encodes with. With the Novatek
* rate control above it yields exactly the stock bytes
* (ab 28 00 0c d2 00 02 25 01 14 00 07 09 75 08 7a 10 f0).
*/
static void nt37705_csot_write_dsc_params(struct mipi_dsi_multi_context *dsi_ctx,
const struct drm_dsc_config *dsc)
{
const u8 buf[] = {
0x91,
(dsc->bits_per_component << 4) | dsc->line_buf_depth,
0x28, /* stock value, same on NT37801 (block pred, 8 bpp) */
dsc->slice_height >> 8, dsc->slice_height & 0xff,
(dsc->first_line_bpg_offset << 4) | 0x2, 0x00,
dsc->initial_dec_delay >> 8, dsc->initial_dec_delay & 0xff,
dsc->scale_increment_interval >> 8,
dsc->scale_increment_interval & 0xff,
dsc->scale_decrement_interval >> 8,
dsc->scale_decrement_interval & 0xff,
dsc->nfl_bpg_offset >> 8, dsc->nfl_bpg_offset & 0xff,
dsc->slice_bpg_offset >> 8, dsc->slice_bpg_offset & 0xff,
dsc->final_offset >> 8, dsc->final_offset & 0xff,
};
mipi_dsi_dcs_write_buffer_multi(dsi_ctx, buf, sizeof(buf));
}
/*
* Full 60 Hz on-command from timing@0 of the stock node, ending with
* sleep-out (100 ms) and display-on as in the vendor sequence.
*/
static int nt37705_csot_on(struct nt37705_csot *ctx)
{
struct mipi_dsi_device *dsi = ctx->dsi;
struct mipi_dsi_multi_context dsi_ctx = { .dsi = dsi };
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xf0, 0x55, 0xaa, 0x52, 0x08, 0x00);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xba, 0x00, 0x6a, 0x00, 0x10, 0x09, 0xb0, 0x10);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x6f, 0x07);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xba, 0x00, 0x6a, 0x00, 0x10, 0x00, 0x20, 0x10);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x6f, 0x0e);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xba, 0x00, 0x4c, 0x00, 0x10, 0x00, 0x2c, 0x10);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xbb, 0x00, 0x6a, 0x00, 0x10, 0x00, 0x20, 0x70);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xc1, 0x00, 0x6a, 0x00, 0x6a, 0x00, 0x4c, 0x00, 0x6a, 0x00, 0x6a);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xf0, 0x55, 0xaa, 0x52, 0x08, 0x08);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xe0, 0x41, 0x73, 0x73, 0x73, 0x83);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xe1, 0x00, 0x5c, 0xc4, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe8, 0x10, 0x00);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xe2, 0x41, 0x8d, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xe3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xe4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xe5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xe6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xf0, 0x55, 0xaa, 0x52, 0x08, 0x07);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x6f, 0x37);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xb7, 0x08, 0x08, 0x08);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xf0, 0x55, 0xaa, 0x52, 0x08, 0x00);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x6f, 0x01);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xc0, 0x20);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xf0, 0x55, 0xaa, 0x52, 0x08, 0x08);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xea, 0x22, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xf0, 0x55, 0xaa, 0x52, 0x08, 0x08);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x6f, 0x21);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xea, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xff, 0xaa, 0x55, 0xa5, 0x83);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x6f, 0x02);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xfe, 0x03);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xf0, 0x55, 0xaa, 0x52, 0x08, 0x08);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xe0, 0x00);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xff, 0xaa, 0x55, 0xa5, 0x81);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x6f, 0x1e);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xfb, 0x0f);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xf0, 0x55, 0xaa, 0x52, 0x08, 0x01);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x6f, 0x05);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xc5, 0x15, 0x15, 0x15);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xff, 0xaa, 0x55, 0xa5, 0x81);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x6f, 0x19);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xfb, 0x00);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xff, 0xaa, 0x55, 0xa5, 0x80);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x6f, 0x2d);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xfc, 0x44);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xff, 0xaa, 0x55, 0xa5, 0x80);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x6f, 0x1a);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xf4, 0x55);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xff, 0xaa, 0x55, 0xa5, 0x83);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x6f, 0x12);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xfe, 0x41);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xff, 0xaa, 0x55, 0xa5, 0x80);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x6f, 0x18);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xf4, 0x30);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xff, 0xaa, 0x55, 0xa5, 0x80);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x6f, 0x19);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xf2, 0x00);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xff, 0xaa, 0x55, 0xa5, 0x80);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x6f, 0x31);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xf8, 0x01, 0x09);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x6f, 0x15);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xf8, 0x01, 0x5e);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x17, 0x10);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x2a, 0x00, 0x00, 0x04, 0x37);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x2b, 0x00, 0x00, 0x09, 0x5f);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x2f, 0x01);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x5f, 0x80);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x35, 0x00);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x51, 0x00, 0x00);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x6f, 0x04);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x51, 0x00, 0x00);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x53, 0x20);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x90, 0x03, 0x03);
nt37705_csot_write_dsc_params(&dsi_ctx, dsi->dsc);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xf0, 0x55, 0xaa, 0x52, 0x08, 0x01);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x6f, 0x02);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xc7, 0x00, 0x00);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x26, 0x00);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0x1f, 0x40);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xf0, 0x55, 0xaa, 0x52, 0x08, 0x00);
mipi_dsi_dcs_write_seq_multi(&dsi_ctx, 0xb2, 0x01, 0x60);
mipi_dsi_dcs_exit_sleep_mode_multi(&dsi_ctx);
mipi_dsi_msleep(&dsi_ctx, 100);
mipi_dsi_dcs_set_display_on_multi(&dsi_ctx);
return dsi_ctx.accum_err;
}
/*
* Bring-up aid: the stock ESD check reads DCS 0x0a and expects 0x9c (booster
* on, sleep out, display on, normal mode). Logs the result either way.
*/
static void nt37705_csot_log_power_mode(struct nt37705_csot *ctx,
const char *when)
{
struct device *dev = &ctx->dsi->dev;
u8 power_mode;
int ret;
ret = mipi_dsi_dcs_get_power_mode(ctx->dsi, &power_mode);
if (ret < 0)
dev_warn(dev, "power mode %s: read failed: %d\n", when, ret);
else
dev_info(dev, "power mode %s: 0x%02x (stock expects 0x9c)\n",
when, power_mode);
}
static int nt37705_csot_off(struct nt37705_csot *ctx)
{
struct mipi_dsi_device *dsi = ctx->dsi;
struct mipi_dsi_multi_context dsi_ctx = { .dsi = dsi };
mipi_dsi_dcs_set_display_off_multi(&dsi_ctx);
mipi_dsi_dcs_enter_sleep_mode_multi(&dsi_ctx);
mipi_dsi_msleep(&dsi_ctx, 120);
return dsi_ctx.accum_err;
}
/*
* Everything is sent from prepare (DSI host already up thanks to
* prepare_prev_first), followed by the standard PPS and compression-mode
* packets, like the mainline NT37801 driver does with the same Novatek
* 0x90/0x91 flow. The downstream Qualcomm driver also sends the PPS on its
* own; it is not part of the vendor on-command.
*/
static int nt37705_csot_prepare(struct drm_panel *panel)
{
struct nt37705_csot *ctx = to_nt37705_csot(panel);
struct device *dev = &ctx->dsi->dev;
struct drm_dsc_picture_parameter_set pps;
s64 elapsed;
int ret;
/* Supplies were cut in probe or unprepare: keep them off >= 100 ms. */
elapsed = ktime_ms_delta(ktime_get(), ctx->off_since);
if (elapsed < 100)
msleep(100 - elapsed);
ret = regulator_bulk_enable(ARRAY_SIZE(nt37705_csot_supplies), ctx->supplies);
if (ret < 0)
return ret;
nt37705_csot_reset(ctx);
ret = nt37705_csot_on(ctx);
if (ret < 0) {
dev_err(dev, "failed to initialize panel: %d\n", ret);
goto err;
}
drm_dsc_pps_payload_pack(&pps, ctx->dsi->dsc);
ret = mipi_dsi_picture_parameter_set(ctx->dsi, &pps);
if (ret < 0) {
dev_err(dev, "failed to transmit PPS: %d\n", ret);
goto err;
}
ret = mipi_dsi_compression_mode(ctx->dsi, true);
if (ret < 0) {
dev_err(dev, "failed to enable compression mode: %d\n", ret);
goto err;
}
msleep(28);
nt37705_csot_log_power_mode(ctx, "after on");
msleep(100);
nt37705_csot_log_power_mode(ctx, "after on +100ms");
msleep(400);
nt37705_csot_log_power_mode(ctx, "after on +500ms");
return 0;
err:
gpiod_set_value_cansleep(ctx->reset_gpio, 1);
regulator_bulk_disable(ARRAY_SIZE(nt37705_csot_supplies), ctx->supplies);
ctx->off_since = ktime_get();
return ret;
}
static int nt37705_csot_unprepare(struct drm_panel *panel)
{
struct nt37705_csot *ctx = to_nt37705_csot(panel);
struct device *dev = &ctx->dsi->dev;
int ret;
ret = nt37705_csot_off(ctx);
if (ret < 0)
dev_err(dev, "failed to un-initialize panel: %d\n", ret);
gpiod_set_value_cansleep(ctx->reset_gpio, 1);
regulator_bulk_disable(ARRAY_SIZE(nt37705_csot_supplies), ctx->supplies);
ctx->off_since = ktime_get();
return 0;
}
/*
* 60 Hz modeline, geometry from the stock timing@0:
* hbp/hfp/hpw = 0x20/0x20/0x10, vbp/vfp/vpw = 0x04/0x0c/0x04 (1080x2400).
*/
static const struct drm_display_mode nt37705_csot_mode = {
.clock = (1080 + 32 + 16 + 32) * (2400 + 4 + 12 + 4) * 60 / 1000,
.hdisplay = 1080,
.hsync_start = 1080 + 32,
.hsync_end = 1080 + 32 + 16,
.htotal = 1080 + 32 + 16 + 32,
.vdisplay = 2400,
.vsync_start = 2400 + 4,
.vsync_end = 2400 + 4 + 12,
.vtotal = 2400 + 4 + 12 + 4,
.width_mm = 69,
.height_mm = 154,
.type = DRM_MODE_TYPE_DRIVER,
};
static int nt37705_csot_get_modes(struct drm_panel *panel,
struct drm_connector *connector)
{
return drm_connector_helper_get_modes_fixed(connector, &nt37705_csot_mode);
}
static const struct drm_panel_funcs nt37705_csot_panel_funcs = {
.prepare = nt37705_csot_prepare,
.unprepare = nt37705_csot_unprepare,
.get_modes = nt37705_csot_get_modes,
};
/*
* DBV (0x51) backlight, 12-bit (0..0x3ffc on the stock panel). The vendor
* "bl-is-exponent"/"bl-inverted-dbv" curves are not reproduced; a raw value
* is written for now so the display has a working brightness control.
*/
static int nt37705_csot_bl_update_status(struct backlight_device *bl)
{
struct mipi_dsi_device *dsi = bl_get_data(bl);
u16 brightness = backlight_get_brightness(bl);
return mipi_dsi_dcs_set_display_brightness_large(dsi, brightness);
}
static int nt37705_csot_bl_get_brightness(struct backlight_device *bl)
{
struct mipi_dsi_device *dsi = bl_get_data(bl);
u16 brightness;
int ret;
ret = mipi_dsi_dcs_get_display_brightness_large(dsi, &brightness);
if (ret < 0)
return ret;
return brightness;
}
static const struct backlight_ops nt37705_csot_bl_ops = {
.update_status = nt37705_csot_bl_update_status,
.get_brightness = nt37705_csot_bl_get_brightness,
};
static struct backlight_device *
nt37705_csot_create_backlight(struct mipi_dsi_device *dsi)
{
struct device *dev = &dsi->dev;
const struct backlight_properties props = {
.type = BACKLIGHT_RAW,
.brightness = 2047,
.max_brightness = 16380,
};
return devm_backlight_device_register(dev, dev_name(dev), dev, dsi,
&nt37705_csot_bl_ops, &props);
}
static int nt37705_csot_probe(struct mipi_dsi_device *dsi)
{
struct device *dev = &dsi->dev;
struct nt37705_csot *ctx;
struct drm_dsc_config *dsc;
int ret;
ctx = devm_drm_panel_alloc(dev, struct nt37705_csot, panel,
&nt37705_csot_panel_funcs,
DRM_MODE_CONNECTOR_DSI);
if (IS_ERR(ctx))
return PTR_ERR(ctx);
ret = devm_regulator_bulk_get_const(dev, ARRAY_SIZE(nt37705_csot_supplies),
nt37705_csot_supplies, &ctx->supplies);
if (ret < 0)
return dev_err_probe(dev, ret, "Failed to get regulators\n");
ctx->reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH);
if (IS_ERR(ctx->reset_gpio))
return dev_err_probe(dev, PTR_ERR(ctx->reset_gpio),
"Failed to get reset-gpios\n");
/*
* The ABL leaves the panel powered and running its splash. Resetting
* it, or even power-cycling it from prepare (DSI PHY already up, lanes
* in LP11), leaves the DDIC dead: no TE, no DCS read response. The same
* power cycle done with the DSI link down (fb blank/unblank) brings it
* up fine every time, so cut the inherited supplies here, while MDSS
* has just been reset and the DSI PHY is still off.
*/
ret = regulator_bulk_enable(ARRAY_SIZE(nt37705_csot_supplies),
ctx->supplies);
if (ret < 0)
return dev_err_probe(dev, ret, "Failed to enable regulators\n");
regulator_bulk_disable(ARRAY_SIZE(nt37705_csot_supplies), ctx->supplies);
ctx->off_since = ktime_get();
/* Stock DSC config: version 1.1, 10 bpc, 8 bpp, novateck-ic flow. */
dsc = devm_kzalloc(dev, sizeof(*dsc), GFP_KERNEL);
if (!dsc)
return -ENOMEM;
dsc->pic_width = 1080;
dsc->pic_height = 2400;
dsc->dsc_version_major = 0x1;
dsc->dsc_version_minor = 0x1;
dsc->slice_height = 12;
dsc->slice_width = 540;
dsc->slice_count = 2;
dsc->bits_per_component = 10;
dsc->bits_per_pixel = 8 << 4;
dsc->block_pred_enable = true;
nt37705_csot_dsc_config_nt(dsc);
dsi->dsc = dsc;
ctx->dsi = dsi;
mipi_dsi_set_drvdata(dsi, ctx);
dsi->lanes = 4;
dsi->format = MIPI_DSI_FMT_RGB888;
dsi->mode_flags = MIPI_DSI_CLOCK_NON_CONTINUOUS | MIPI_DSI_MODE_LPM;
ctx->panel.prepare_prev_first = true;
ctx->panel.backlight = nt37705_csot_create_backlight(dsi);
if (IS_ERR(ctx->panel.backlight))
return dev_err_probe(dev, PTR_ERR(ctx->panel.backlight),
"Failed to create backlight\n");
drm_panel_add(&ctx->panel);
ret = devm_mipi_dsi_attach(dev, dsi);
if (ret < 0) {
drm_panel_remove(&ctx->panel);
return dev_err_probe(dev, ret, "Failed to attach to DSI host\n");
}
return 0;
}
static void nt37705_csot_remove(struct mipi_dsi_device *dsi)
{
struct nt37705_csot *ctx = mipi_dsi_get_drvdata(dsi);
drm_panel_remove(&ctx->panel);
}
static const struct of_device_id nt37705_csot_of_match[] = {
{ .compatible = "novatek,nt37705" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, nt37705_csot_of_match);
static struct mipi_dsi_driver nt37705_csot_driver = {
.probe = nt37705_csot_probe,
.remove = nt37705_csot_remove,
.driver = {
.name = "panel-novatek-nt37705",
.of_match_table = nt37705_csot_of_match,
},
};
module_mipi_dsi_driver(nt37705_csot_driver);
MODULE_AUTHOR("snlavaise <snlavaise@gmail.com>");
MODULE_DESCRIPTION("DRM driver for the CSOT NT37705 (Novatek) cmd mode DSC panel on Motorola rtwo");
MODULE_LICENSE("GPL");
@@ -1,5 +1,62 @@
# Motorola rtwo bring-up fragment, appended to config-motorola-rtwo.aarch64. # Motorola rtwo bring-up fragment, appended to config-motorola-rtwo.aarch64.
# --- hardening OFF (bring-up diagnostic) --------------------------------
# config-motorola-rtwo.aarch64 (heredado de linux-postmarketos-mainline,
# nunca revisado a proposito para este equipo) trae CFI/BTI/PAC/MTE/SCS
# encendidos incluso para el propio codigo del kernel. Todas estas
# protecciones exigen que CUALQUIER cosa a la que el kernel salte o
# retorne indirectamente (trampolines de PSCI/SMCCC, callbacks del
# hipervisor Gunyah - el rtwo corre como VM primaria) respete las mismas
# convenciones (firmas CFI, landing pads BTI, x18 reservado para SCS,
# punteros autenticados). Si algun trampolin de firmware/hipervisor no
# fue compilado con las mismas convenciones, la CPU dispara una excepcion
# de hardware inmediata - encaja con un reset rapido y consistente que no
# cambio en absoluto al sacar reguladores/USB/UART/ramoops no-map. Apagado
# para aislar esto como variable; si el equipo arranca con esto apagado,
# reactivar de a una por vez para encontrar cual puntual es la culpable.
# CONFIG_CFI is not set
# CONFIG_ARM64_BTI_KERNEL is not set
# CONFIG_ARM64_PTR_AUTH_KERNEL is not set
# CONFIG_ARM64_MTE is not set
# CONFIG_SHADOW_CALL_STACK is not set
# --- kernel lockdown OFF (bring-up diagnostic) --------------------------
# config-motorola-rtwo.aarch64 trae CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITY=y
# a secas, asi que el kernel arranca en lockdown "integrity" SIEMPRE,
# independientemente del estado de Secure Boot que reporte U-Boot vía EFI.
# Lockdown activa LOCKDOWN_MODULE_SIGNATURE: cualquier .ko cuya firma no
# verifique contra el certs/signing_key.pem de ESTE build puntual se
# rechaza de plano ("Lockdown: modprobe: unsigned module loading is
# restricted; see man kernel_lockdown.7"), aunque ese mismo modulo haya
# cargado bien ayer. Esto nos exploto justo al reconstruir el kernel (para
# soporte UFS) mientras dejabamos a proposito el ramdisk hand-patcheado
# (dropbear + retry loop del UDC) SIN regenerar: los modulos empaquetados
# ahi quedaron firmados con la clave del build ANTERIOR, asi que el kernel
# nuevo desconfia de todos por igual -- tanto los de UFS recien habilitados
# como los del PHY USB que hasta ese rebuild cargaban sin problema. Apagado
# mientras dure el bring-up activo (vamos a seguir reconstruyendo el kernel
# sin resincronizar el ramdisk cada vez); nada aca necesita lockdown todavia.
CONFIG_LOCK_DOWN_KERNEL_FORCE_NONE=y
# CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITY is not set
# --- VA/PA de 52-bit -> 48-bit (bring-up diagnostic) --------------------
# config-motorola-rtwo.aarch64 trae VA_BITS/PA_BITS en 52 (LPA2), heredado
# del defconfig generico. device-motorola-dubai (unico port mainline de
# Motorola que arranca, mismo ABL/MbmPkg, aunque SIN hipervisor Gunyah de
# por medio) usa 48/48. El rtwo SI corre como VM primaria de Gunyah
# (confirmado: nodo qcom,gh-watchdog real en el DT stock), y el hipervisor
# es quien gestiona el espacio de direcciones intermedio (IPA) del huesped
# - si Gunyah en este equipo solo soporta 48-bit para la VM primaria,
# forzar 52-bit podria romper el establecimiento de la MMU en el arranque
# en ensamblador, ANTES de cualquier codigo en C y antes de parsear el
# device tree - coherente con un reset durisimo que no cambio en absoluto
# con nada de lo tocado en el .dts hasta ahora.
CONFIG_ARM64_VA_BITS_48=y
# CONFIG_ARM64_VA_BITS_52 is not set
CONFIG_ARM64_PA_BITS_48=y
# CONFIG_ARM64_PA_BITS_52 is not set
# CONFIG_ARM64_LPA2 is not set
# --- pstore / ramoops ------------------------------------------------------- # --- pstore / ramoops -------------------------------------------------------
# Capture the kernel console into the reserved ramoops region (0xae000000) # Capture the kernel console into the reserved ramoops region (0xae000000)
# so a failed boot can be read back from a later pmOS boot # so a failed boot can be read back from a later pmOS boot
@@ -14,16 +71,48 @@ CONFIG_PSTORE_COMPRESS=y
# sm8550.dtsi has the APSS watchdog node; build the driver in so the kernel # sm8550.dtsi has the APSS watchdog node; build the driver in so the kernel
# pets it early (the bootloader may hand off with it armed). # pets it early (the bootloader may hand off with it armed).
CONFIG_QCOM_WDT=y CONFIG_QCOM_WDT=y
# CONFIG_QCOM_WDT above is the wrong watchdog for what actually matters here:
# it's DT-matched to the APSS timer at 0x17c10000, but rtwo boots as a Gunyah
# primary VM (qcom,gh-watchdog node confirmed in the stock DT) and the real
# watchdog guarding HLOS handoff is the HYPERVISOR's, reached only via SMCCC
# (ARM_SMCCC_OWNER_VENDOR_HYP), never via a DT node - it is registered by
# qcom_scm_gunyah_wdt_init() (drivers/firmware/qcom/qcom_scm.c), which
# unconditionally runs once CONFIG_QCOM_SCM=y probes AND arm_smccc_hypervisor_
# has_uuid() confirms Gunyah - both true for us. That function only skips
# registration if it finds an of_device_is_available() node compatible with
# "qcom,kpss-wdt" or "arm,sbsa-gwdt" - our stock DT's gh-watchdog node is
# neither, so it registers the "gunyah-wdt" platform_device regardless.
# Without CONFIG_GUNYAH_WATCHDOG, nothing ever binds to that platform_device,
# so if Gunyah/firmware hands off HLOS with the hyp watchdog already armed
# (a plausible safety net for a VM that never comes up), nothing ever calls
# gunyah_wdt_stop()/pings it, and it fires -> reset with no kernel-visible
# fault. gunyah_wdt_probe() calls gunyah_wdt_stop() unconditionally as its
# first action after the SMCCC status check succeeds - it does NOT arm or
# start the watchdog itself, so turning this on cannot make anything worse,
# it can only disarm a pre-armed timer earlier than before.
# Depends only on (ARCH_QCOM || COMPILE_TEST) && HAVE_ARM_SMCCC, both already
# satisfied; selects WATCHDOG_CORE (already =y via CONFIG_QCOM_WDT above).
CONFIG_GUNYAH_WATCHDOG=y
# --- framebuffer console ---------------------------------------------- # --- framebuffer console ----------------------------------------------
# Reuse the bootloader framebuffer as an early console (simple-framebuffer # Reactivado 2026-09-17 junto con framebuffer@b8000000 en el .dts (ver ese
# node in the .dts). No panel driver needed. # archivo): con el chainload de U-Boot confirmado en hardware, este
CONFIG_FB=y # framebuffer ya esta encendido y con el formato correcto antes de que
CONFIG_FB_SIMPLE=y # arranque nuestro kernel, asi que simpledrm solo tiene que mapearlo.
#
# CONFIG_FB / CONFIG_SYSFB_SIMPLEFB no hacen falta: DRM_SIMPLEDRM solo
# depende de DRM && MMU (ambos ya =y en la config base) y bindea el nodo
# "simple-framebuffer" del DT directamente, sin pasar por el fbdev legacy
# ni por el sysfb de x86 (SYSFB_SIMPLEFB es para EFI/VESA en x86, no
# aplica aca). CONFIG_FRAMEBUFFER_CONSOLE ya viene =y de la config base
# (usa el struct fb_info que expone DRM_FBDEV_EMULATION, tambien ya =y).
#
# CONFIG_FB_SIMPLE se deja APAGADO a proposito (la config base ya lo trae
# asi): compite con DRM_SIMPLEDRM por el mismo nodo "simple-framebuffer"
# del DT y gana el que bindee primero -- confundio un intento anterior de
# este mismo bring-up (pkgrel 22). Con los dos no hace falta, uno alcanza.
CONFIG_DRM_SIMPLEDRM=y CONFIG_DRM_SIMPLEDRM=y
CONFIG_SYSFB_SIMPLEFB=y # CONFIG_FRAMEBUFFER_CONSOLE_DETECT_PRIMARY is not set
CONFIG_FRAMEBUFFER_CONSOLE=y
CONFIG_FRAMEBUFFER_CONSOLE_DETECT_PRIMARY=y
# --- misc bring-up aids --------------------------------------------- # --- misc bring-up aids ---------------------------------------------
CONFIG_MAGIC_SYSRQ=y CONFIG_MAGIC_SYSRQ=y
@@ -81,3 +170,155 @@ CONFIG_DEBUG_INFO_NONE=y
# CONFIG_ARCH_VISCONTI is not set # CONFIG_ARCH_VISCONTI is not set
# CONFIG_ARCH_XGENE is not set # CONFIG_ARCH_XGENE is not set
# CONFIG_ARCH_ZYNQMP is not set # CONFIG_ARCH_ZYNQMP is not set
# --- module compression: OFF ------------------------------------------
# The base config (from linux-postmarketos-mainline) turns on
# CONFIG_MODULE_COMPRESS + CONFIG_MODULE_COMPRESS_ZSTD +
# CONFIG_MODULE_COMPRESS_ALL, so 'make modules_install' writes *.ko.zst.
# The pmOS initramfs loads modules with busybox's modprobe applet
# ('/bin/busybox --install -s' in /init, then 'modprobe -a' from
# load_modules): that applet only knows the plain '.ko' suffix and only
# decompresses gzip/bzip2/xz/lzma by magic - this busybox has NO zstd
# ("no gzip/bzip2/xz magic" is the error it prints). CONFIG_MODULE_DECOMPRESS
# does not help either: busybox reads the file itself and calls init_module(),
# never finit_module(MODULE_INIT_COMPRESSED_FILE).
#
# So with zstd every module in initramfs.load silently fails to load:
# ufs-qcom/qcom_ice/phy-qcom-qmp-ufs -> no root device -> "VFS: Unable to
# mount root fs on unknown-block(0,0)", and phy-snps-eusb2 -> no UDC ->
# no usb0, no telnet on 172.16.42.1.
#
# 7.2 has no MODULE_COMPRESS_NONE symbol (it was replaced by the
# MODULE_COMPRESS bool + a type choice), so turn the bool off; olddefconfig
# then drops COMPRESS_ZSTD / COMPRESS_ALL / DECOMPRESS with it.
# CONFIG_MODULE_COMPRESS is not set
# --- RETRACTED 2026-09-20: CONFIG_QCOMTEE / CONFIG_ARM_QCOM_CPUFREQ_HW --
# were NOT disabled here in the end. Both were disabled for a while
# (2026-09-18) after the screen appeared to freeze right after "qcomtee:
# QTEE version 5.2.0", then right after "cpu cpu0: EM: created perf
# domain" -- diagnosed purely from photos of the framebuffer console, no
# serial/SSH/telnet available at the time to confirm the system was
# actually stuck.
#
# That diagnosis didn't survive scrutiny: in every one of those "hung"
# attempts the device stayed reachable over ping the whole time. A kernel
# truly wedged in a probe function (e.g. an SMC call that never returns)
# would eventually stop servicing network softirqs too, at least once
# nothing else is left running; ping staying up is much better explained
# by the boot actually completing (or getting close) while only the
# on-screen console stopped updating -- unreachable over SSH because
# sshd/networking-online wasn't there yet at that point in the bring-up,
# and unreachable over telnet because no telnet/debug shell was installed
# in userspace yet (added later, see device-motorola-rtwo's
# debug-telnetd.service / dropbear work).
#
# Separately and since confirmed on 2026-09-20 (see sm8550-motorola-rtwo.dts,
# the &dispcc override): the inherited U-Boot framebuffer genuinely does
# freeze on this board, root-caused to dispcc (Display Clock Controller)
# touching disp_cc_mdss_mdp_clk_src -- the MDP pixel clock still actively
# driving that inherited buffer -- during its own probe. CONFIG_QCOMTEE=m
# is a *module*, only autoloaded once udev coldplugs devices in the real
# systemd rootfs; CONFIG_ARM_QCOM_CPUFREQ_HW=y is built-in but its probe
# depends on OPP/interconnect data that isn't ready until roughly the same
# late-boot window (matches the global deferred-probe-timeout retry seen
# at ~11s in current dmesg). Both cluster in the same few seconds of boot
# as dispcc's own module autoload -- entirely consistent with "whatever
# happened to be the last line printed before the screen froze" rather
# than either of them being the actual cause of a hang.
#
# Re-enabled to their config-motorola-rtwo.aarch64 defaults
# (CONFIG_QCOMTEE=m, CONFIG_ARM_QCOM_CPUFREQ_HW=y). Re-test now that a
# debug telnet/SSH shell exists in userspace, so aliveness can be checked
# directly instead of inferred from a photo of a console that we now know
# can freeze independently of whether the system is actually up.
# --- ath12k: debug messages ON (WiFi bring-up diagnostic) ---------------
# The whole "WMI credit exhaustion" diagnosis so far was done BLIND: the
# only hard evidence was ath12k_wmi_cmd_send() printing "wmi command 16387
# timeout" after WMI_SEND_TIMEOUT_HZ, plus -11/-EAGAIN, which in this
# driver can ONLY come from ath12k_htc_send()'s "insufficient credits"
# path (wmi.c:412 wait_event_timeout -> cmd_send_nowait -> htc_send; no
# other error path in that chain returns -EAGAIN). Everything else --
# how many credits the endpoint started with, whether a single credit
# report ever came back -- was inferred, because every one of those facts
# is printed by an ath12k_dbg() call and config-motorola-rtwo.aarch64
# (inherited, never reviewed for this device) has
# "# CONFIG_ATH12K_DEBUG is not set", which compiles __ath12k_dbg() down
# to an empty inline (debug.h). We were reading a silent driver.
#
# With this on, the messages that answer the open question directly:
# htc.c:543 "Target ready! transmit resources: %d size:%d"
# -> htc->total_transmit_credits / target_credit_size, the
# initial credit budget the firmware advertises.
# htc.c:104 "htc ep %d consumed %d credits (total %d)" (every TX)
# htc.c:166 "htc ep %d got %d credits (total %d)" (every credit
# report parsed out of an RX trailer)
# htc.c:96 "htc insufficient credits ep %d required %d available %d"
# all under ATH12K_DBG_HTC (0x4). Falsifiable prediction to check against
# them: if the firmware never returns a single credit, the number of WMI
# commands that succeed before the wall (INIT, SET_HW_MODE, ..., then the
# one PMF_QOS in ath12k_mac_start) should come out EXACTLY equal to the
# advertised initial budget -- and not one "got %d credits" line should
# ever appear. If instead credits do come back and then stop, the failure
# is somewhere else entirely and the "firmware never reports" theory dies.
#
# Enabled separately at runtime via the debug_mask module param (0644, so
# also writable through /sys/module/ath12k/parameters/debug_mask, and
# settable from the kernel cmdline as ath12k.debug_mask=0x66 even though
# ath12k is a module). Bits (debug.h): WMI 0x2 | HTC 0x4 | BOOT 0x20 |
# QMI 0x40. Cost when the mask is 0 is one branch per call site, and this
# is a bring-up kernel -- performance is not the constraint here.
CONFIG_ATH12K_DEBUG=y
# Firmware coredump on assert. WANT_DEV_COREDUMP/DEV_COREDUMP are already
# =y in the base config, so this only adds ath12k's own collector. It
# costs nothing while nothing crashes, and it settles a question the
# current evidence cannot: whether the firmware silently asserted at some
# point during bring-up (a dead firmware explains "credits never come
# back" completely). A dump would show up under /sys/class/devcoredump/.
CONFIG_ATH12K_COREDUMP=y
# Native display bring-up (CSOT NT37705 panel, see panel-novatek-nt37705.c).
# DRM_MSM is present as =m in the mainline base config; build it in so the
# mdss/dsi0 probe runs from the boot (the frame is committed by MSM itself,
# no simpledrm handover needed). DRM_DISPLAY_DSC_HELPER is already =y.
CONFIG_BACKLIGHT_CLASS_DEVICE=y
CONFIG_DRM_MSM=y
CONFIG_DRM_MSM_DSI=y
CONFIG_DRM_MIPI_DSI=y
CONFIG_DRM_PANEL=y
CONFIG_DRM_PANEL_NOVATEK_NT37705=y
# dispcc/gpucc built in (2026-09-25): as =m they only load once the rootfs is
# mounted (~17 s), after the 10 s deferred_probe_timeout. Their GDSCs are the
# power-domains of mdss and of the GPU SMMU, and genpd returns -ETIMEDOUT once
# the timeout expires: seen in r43 as "arm-smmu 3da0000.iommu: probe ... failed
# with error -110". Built-in DRM_MSM would hit the same wall on MDSS_GDSC.
CONFIG_SM_DISPCC_8550=y
CONFIG_SM_GPUCC_8550=y
# DRM_MSM "depends on QCOM_LLCC || QCOM_LLCC=n" (same for QCOM_OCMEM): with
# both =m in the base config, DRM_MSM=y above was silently capped to =m by
# olddefconfig (r44: msm.ko in the rootfs, nothing drove the panel from the
# initramfs). Build them in so DRM_MSM can really be =y.
CONFIG_QCOM_LLCC=y
CONFIG_QCOM_OCMEM=y
# DisplayPort por USB-C (2026-09-25). Cadena: mdss_dp0 -> QMP USB3/DP combo
# PHY -> redriver Parade PS5169 (typec-mux-ps5169.c, integrado en este
# paquete) -> conector de pmic-glink (altmode + HPD), con el FSA4480 moviendo
# AUX por los SBU. Todo =y a proposito: el DP es un componente de msm, y msm
# (panel interno incluido) no se enlaza hasta que el DP encuentra su PHY y
# su cadena de bridges; pmic_glink_altmode solo registra su bridge HPD
# despues de conseguir el FSA4480 y el redriver. Como =m, eso esperaria a
# que monte el rootfs (y en la debug shell no llegaria nunca).
CONFIG_TYPEC=y
CONFIG_PHY_QCOM_QMP=y
CONFIG_PHY_QCOM_QMP_COMBO=y
CONFIG_DRM_AUX_BRIDGE=y
CONFIG_DRM_AUX_HPD_BRIDGE=y
CONFIG_QCOM_PMIC_GLINK=y
CONFIG_I2C_QCOM_GENI=y
CONFIG_TYPEC_MUX_FSA4480=y
CONFIG_TYPEC_MUX_PS5169=y
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,417 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Parade PS5169 USB 3.1 Gen2 / DP 1.4 Type-C linear redriver
*
* Mainline typec switch + retimer front-end for the redriver sitting
* between the SM8550 QMP USB3/DP combo PHY and the USB-C connector of the
* Motorola rtwo (Moto Edge 40 Pro). Modelled on nb7vpq904m.c; the register
* programming (CONFIG_REG bits, 0xa0/0xa1 lane setup, chip id) comes from
* Motorola's downstream drivers/usb/redriver/ps5169.c, and the init table
* is the 'config-seq' of the stock rtwo device tree.
*/
#include <linux/delay.h>
#include <linux/gpio/consumer.h>
#include <linux/i2c.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/regmap.h>
#include <linux/regulator/consumer.h>
#include <linux/usb/typec_dp.h>
#include <linux/usb/typec_mux.h>
#include <linux/usb/typec_retimer.h>
#include <drm/bridge/aux-bridge.h>
#define PS5169_CONFIG_REG 0x40
#define PS5169_PD_ACTIVE BIT(7)
#define PS5169_USB3_EN BIT(6)
#define PS5169_DP_EN BIT(5)
#define PS5169_FLIPPED BIT(4)
#define PS5169_CONFIG_MASK (PS5169_PD_ACTIVE | PS5169_USB3_EN | \
PS5169_DP_EN | PS5169_FLIPPED)
/* Lane setup written by the downstream driver next to every mode change. */
#define PS5169_LANE_CFG0_REG 0xa0
#define PS5169_LANE_CFG1_REG 0xa1
#define PS5169_CHIPID_LO_REG 0xac
#define PS5169_CHIPID_HI_REG 0xad
#define PS5169_CHIP_ID 0x6987
/*
* Stock rtwo 'config-seq' (reg, value, delay in us): soft reset through
* 0x9d, then EQ/gain tuning for this board's traces.
*/
static const struct reg_sequence ps5169_init_seq[] = {
{ 0x9d, 0x80, 10000 },
{ 0x9d, 0x00 },
{ 0x40, 0x80 },
{ 0x8d, 0x01 },
{ 0x90, 0x01 },
{ 0x51, 0x87 },
{ 0x50, 0x20 },
{ 0x54, 0x11 },
{ 0x5d, 0x66 },
{ 0x52, 0x50 },
{ 0x55, 0x00 },
{ 0x56, 0x00 },
{ 0x57, 0x00 },
{ 0x58, 0x00 },
{ 0x59, 0x00 },
{ 0x5a, 0x00 },
{ 0x5b, 0x00 },
{ 0x5e, 0x07 },
{ 0x5f, 0x00 },
{ 0x60, 0x00 },
{ 0x61, 0x03 },
{ 0x65, 0x40 },
{ 0x66, 0x00 },
{ 0x67, 0x03 },
{ 0x75, 0x0c },
{ 0x77, 0x00 },
{ 0x78, 0x7c },
};
struct ps5169 {
struct i2c_client *client;
struct regmap *regmap;
struct gpio_desc *enable_gpio;
struct regulator *vcc_supply;
struct typec_switch_dev *sw;
struct typec_retimer *retimer;
struct typec_switch *typec_switch;
struct typec_mux *typec_mux;
struct mutex lock; /* protects the fields below */
bool powered;
unsigned int config; /* CONFIG_REG value last programmed, 0 = none */
enum typec_orientation orientation;
unsigned long mode;
unsigned int svid;
};
static void ps5169_power(struct ps5169 *ps, bool on)
{
if (ps->powered == on)
return;
gpiod_set_value_cansleep(ps->enable_gpio, on);
if (on)
usleep_range(10000, 11000);
ps->powered = on;
}
static int ps5169_set(struct ps5169 *ps)
{
bool reverse = ps->orientation == TYPEC_ORIENTATION_REVERSE;
unsigned int val;
u8 cfg0, cfg1;
int ret;
/* Only a disconnect (no orientation) powers the redriver down. */
if (ps->orientation == TYPEC_ORIENTATION_NONE) {
ps5169_power(ps, false);
ps->config = 0;
return 0;
}
switch (ps->mode) {
/*
* With a plain USB partner (OTG cable, pendrive) the rtwo ADSP did
* not bring the redriver up: no TYPEC_STATE_USB arrived, it stayed
* in TYPEC_STATE_SAFE (NO_CONN) with a valid orientation.
* Downstream routes USB3 on every connect (notify_connect: op mode
* NONE -> USB3), so do the same instead of leaving the chip off,
* which kept the SuperSpeed lanes dead.
*/
case TYPEC_STATE_SAFE:
case TYPEC_STATE_USB:
val = PS5169_USB3_EN;
cfg0 = 0;
cfg1 = 0;
break;
case TYPEC_DP_STATE_C:
case TYPEC_DP_STATE_E:
if (ps->svid != USB_TYPEC_DP_SID)
return -EINVAL;
val = PS5169_DP_EN;
cfg0 = 0x00;
cfg1 = 0x04;
break;
case TYPEC_DP_STATE_D:
case TYPEC_DP_STATE_F:
if (ps->svid != USB_TYPEC_DP_SID)
return -EINVAL;
val = PS5169_USB3_EN | PS5169_DP_EN;
cfg0 = 0x00;
cfg1 = 0x04;
break;
default:
return -EOPNOTSUPP;
}
if (reverse)
val |= PS5169_FLIPPED;
val |= PS5169_PD_ACTIVE;
if (ps->powered && ps->config == val)
return 0;
/*
* The downstream driver re-runs the full init on every mode change,
* and power cycles the chip first when it is already running
* (ps5169_host_work: enable low for 2 ms, then high again).
*/
if (ps->powered) {
ps5169_power(ps, false);
usleep_range(2000, 2500);
}
ps->config = 0;
ps5169_power(ps, true);
ret = regmap_multi_reg_write(ps->regmap, ps5169_init_seq,
ARRAY_SIZE(ps5169_init_seq));
if (ret)
goto err;
ret = regmap_update_bits(ps->regmap, PS5169_CONFIG_REG,
PS5169_CONFIG_MASK, val);
if (ret)
goto err;
if (val & PS5169_DP_EN) {
ret = regmap_write(ps->regmap, PS5169_LANE_CFG0_REG, cfg0);
if (!ret)
ret = regmap_write(ps->regmap, PS5169_LANE_CFG1_REG, cfg1);
if (ret)
goto err;
}
ps->config = val;
dev_dbg(&ps->client->dev, "mode %lu orientation %d -> config 0x%02x\n",
ps->mode, ps->orientation, val);
return 0;
err:
dev_err(&ps->client->dev, "failed to program mode %lu: %d\n",
ps->mode, ret);
return ret;
}
static int ps5169_sw_set(struct typec_switch_dev *sw,
enum typec_orientation orientation)
{
struct ps5169 *ps = typec_switch_get_drvdata(sw);
int ret;
ret = typec_switch_set(ps->typec_switch, orientation);
if (ret)
return ret;
dev_dbg(&ps->client->dev, "switch: orientation %d\n", orientation);
mutex_lock(&ps->lock);
if (ps->orientation != orientation) {
ps->orientation = orientation;
ret = ps5169_set(ps);
}
mutex_unlock(&ps->lock);
return ret;
}
static int ps5169_retimer_set(struct typec_retimer *retimer,
struct typec_retimer_state *state)
{
struct ps5169 *ps = typec_retimer_get_drvdata(retimer);
struct typec_mux_state mux_state;
int ret = 0;
dev_dbg(&ps->client->dev, "retimer: mode %lu svid 0x%04x\n",
state->mode, state->alt ? state->alt->svid : 0);
mutex_lock(&ps->lock);
if (ps->mode != state->mode) {
ps->mode = state->mode;
ps->svid = state->alt ? state->alt->svid : 0;
ret = ps5169_set(ps);
}
mutex_unlock(&ps->lock);
if (ret)
return ret;
mux_state.alt = state->alt;
mux_state.data = state->data;
mux_state.mode = state->mode;
return typec_mux_set(ps->typec_mux, &mux_state);
}
static const struct regmap_config ps5169_regmap = {
.max_register = 0xff,
.reg_bits = 8,
.val_bits = 8,
};
static int ps5169_probe(struct i2c_client *client)
{
struct device *dev = &client->dev;
struct typec_switch_desc sw_desc = { };
struct typec_retimer_desc retimer_desc = { };
unsigned int lo, hi;
struct ps5169 *ps;
int ret;
ps = devm_kzalloc(dev, sizeof(*ps), GFP_KERNEL);
if (!ps)
return -ENOMEM;
ps->client = client;
ps->mode = TYPEC_STATE_SAFE;
ps->orientation = TYPEC_ORIENTATION_NONE;
mutex_init(&ps->lock);
ps->regmap = devm_regmap_init_i2c(client, &ps5169_regmap);
if (IS_ERR(ps->regmap))
return dev_err_probe(dev, PTR_ERR(ps->regmap),
"failed to allocate register map\n");
ps->enable_gpio = devm_gpiod_get(dev, "enable", GPIOD_OUT_LOW);
if (IS_ERR(ps->enable_gpio))
return dev_err_probe(dev, PTR_ERR(ps->enable_gpio),
"unable to acquire enable gpio\n");
ps->vcc_supply = devm_regulator_get(dev, "vcc");
if (IS_ERR(ps->vcc_supply))
return dev_err_probe(dev, PTR_ERR(ps->vcc_supply),
"unable to acquire vcc supply\n");
ps->typec_switch = fwnode_typec_switch_get(dev->fwnode);
if (IS_ERR(ps->typec_switch))
return dev_err_probe(dev, PTR_ERR(ps->typec_switch),
"failed to acquire orientation-switch\n");
ps->typec_mux = fwnode_typec_mux_get(dev->fwnode);
if (IS_ERR(ps->typec_mux)) {
ret = dev_err_probe(dev, PTR_ERR(ps->typec_mux),
"failed to acquire mode-switch\n");
goto err_switch_put;
}
ret = regulator_enable(ps->vcc_supply);
if (ret) {
dev_err(dev, "failed to enable vcc: %d\n", ret);
goto err_mux_put;
}
/*
* Identify the chip, then leave it powered down until a partner
* shows up (same as downstream). An I2C failure is only reported:
* the DRM bridge chain of the DP controller runs through this device,
* so failing the probe here would keep the whole MSM display
* (internal panel included) from binding.
*/
ps5169_power(ps, true);
ret = regmap_read(ps->regmap, PS5169_CHIPID_LO_REG, &lo);
if (!ret)
ret = regmap_read(ps->regmap, PS5169_CHIPID_HI_REG, &hi);
if (ret)
dev_warn(dev, "failed to read chip id: %d\n", ret);
else if (((hi << 8) | lo) != PS5169_CHIP_ID)
dev_warn(dev, "unexpected chip id 0x%04x\n", (hi << 8) | lo);
else
dev_info(dev, "PS5169 redriver, chip id 0x%04x\n", PS5169_CHIP_ID);
ps5169_power(ps, false);
ret = drm_aux_bridge_register(dev);
if (ret)
goto err_disable_vcc;
sw_desc.drvdata = ps;
sw_desc.fwnode = dev->fwnode;
sw_desc.set = ps5169_sw_set;
ps->sw = typec_switch_register(dev, &sw_desc);
if (IS_ERR(ps->sw)) {
ret = dev_err_probe(dev, PTR_ERR(ps->sw),
"failed to register typec switch\n");
goto err_disable_vcc;
}
retimer_desc.drvdata = ps;
retimer_desc.fwnode = dev->fwnode;
retimer_desc.set = ps5169_retimer_set;
ps->retimer = typec_retimer_register(dev, &retimer_desc);
if (IS_ERR(ps->retimer)) {
ret = dev_err_probe(dev, PTR_ERR(ps->retimer),
"failed to register typec retimer\n");
goto err_switch_unregister;
}
i2c_set_clientdata(client, ps);
return 0;
err_switch_unregister:
typec_switch_unregister(ps->sw);
err_disable_vcc:
regulator_disable(ps->vcc_supply);
err_mux_put:
typec_mux_put(ps->typec_mux);
err_switch_put:
typec_switch_put(ps->typec_switch);
return ret;
}
static void ps5169_remove(struct i2c_client *client)
{
struct ps5169 *ps = i2c_get_clientdata(client);
typec_retimer_unregister(ps->retimer);
typec_switch_unregister(ps->sw);
gpiod_set_value_cansleep(ps->enable_gpio, 0);
regulator_disable(ps->vcc_supply);
typec_mux_put(ps->typec_mux);
typec_switch_put(ps->typec_switch);
}
static void ps5169_shutdown(struct i2c_client *client)
{
struct ps5169 *ps = i2c_get_clientdata(client);
if (ps)
gpiod_set_value_cansleep(ps->enable_gpio, 0);
}
static const struct i2c_device_id ps5169_table[] = {
{ .name = "ps5169" },
{ }
};
MODULE_DEVICE_TABLE(i2c, ps5169_table);
static const struct of_device_id ps5169_of_table[] = {
{ .compatible = "parade,ps5169" },
{ }
};
MODULE_DEVICE_TABLE(of, ps5169_of_table);
static struct i2c_driver ps5169_driver = {
.driver = {
.name = "ps5169",
.of_match_table = ps5169_of_table,
},
.probe = ps5169_probe,
.remove = ps5169_remove,
.shutdown = ps5169_shutdown,
.id_table = ps5169_table,
};
module_i2c_driver(ps5169_driver);
MODULE_DESCRIPTION("Parade PS5169 Type-C redriver driver");
MODULE_LICENSE("GPL");