linux-postmarketos-qcom-sm8550: nt37705: support 48/90/120/165 Hz

Expose the five timings of the vendor CSOT panel (48, 60, 90, 120 and
165 Hz, 60 preferred). All of them share porches, PHY timings and the
DSI clock; only the timing-switch commands differ (2f XX and e0 YY on
page 08), so prepare() picks them from the refresh rate of the CRTC
mode. Changing the rate is a full modeset (panel power cycle).

Tested on hardware: all rates selectable from GNOME display settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 01:50:25 -03:00
co-authored by Claude Opus 5.5
parent 98b3104e94
commit 55cc4b7703
2 changed files with 109 additions and 26 deletions
@@ -5,7 +5,7 @@ maintainer="snlavaise <snlavaise@gmail.com>"
_flavor="postmarketos-qcom-sm8550"
pkgname=linux-$_flavor
pkgver=7.2
pkgrel=48
pkgrel=49
pkgdesc="Mainline kernel for Qualcomm SM8550 (Snapdragon 8 Gen 2) + Motorola rtwo"
arch="aarch64"
_carch="arm64"
@@ -174,6 +174,6 @@ cf4889c4dca8d3bac7544d71284e6b592d6014d9ad94badfc6d2cbe56a8402c61e4c0811c2f48d33
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
f2ec70d834577378c564e8cab51879f14815693a04af5382b3a778a598a1b3c86585ff9dadf004fa526920778d6540cc44db769a4f75c2af883dcf52c2b70af8 panel-novatek-nt37705.c
dd95e80f62879d5167e7bfc7d74b4a64669a43dbf5aacb6a13e62d1ed4a88677ef1042f8cecc46e05e3d12ba75f070a5927df308c90fe846af93b78c6b310e80 typec-mux-ps5169.c
"
@@ -8,10 +8,11 @@
* 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).
* see pmos/panel-nt37705-v1-dump.md. The stock node has five timings
* (48/60/90/120/165 Hz) with the same porches, PHY timings and DSI clock;
* their on-commands only differ in two bytes, the frame rate select (0x2f)
* and the first 0xe0 parameter of CMD page 8. All five are exposed; 60 Hz
* stays the preferred mode.
*
* DSC: besides the Novatek MCS setup in the on command (0x90 enable, 0x91
* parameters), the standard PPS and compression-mode packets are sent after
@@ -30,6 +31,9 @@
#include <video/mipi_display.h>
#include <drm/display/drm_dsc.h>
#include <drm/drm_atomic.h>
#include <drm/drm_connector.h>
#include <drm/drm_crtc.h>
#include <drm/display/drm_dsc_helper.h>
#include <drm/drm_mipi_dsi.h>
#include <drm/drm_modes.h>
@@ -41,6 +45,7 @@ struct nt37705_csot {
struct mipi_dsi_device *dsi;
struct gpio_desc *reset_gpio;
struct regulator_bulk_data *supplies;
struct drm_connector *connector;
ktime_t off_since;
};
@@ -201,10 +206,29 @@ static void nt37705_csot_write_dsc_params(struct mipi_dsi_multi_context *dsi_ctx
}
/*
* Full 60 Hz on-command from timing@0 of the stock node, ending with
* Per-rate bytes of the stock on-command (and timing-switch-command):
* 0x2f frame rate select, and the 0xe0 parameter written on CMD page 8.
*/
struct nt37705_csot_rate {
unsigned int vrefresh;
u8 frame_rate;
u8 e0;
};
static const struct nt37705_csot_rate nt37705_csot_rates[] = {
{ .vrefresh = 60, .frame_rate = 0x01, .e0 = 0x00 }, /* timing@0 */
{ .vrefresh = 120, .frame_rate = 0x02, .e0 = 0x41 }, /* timing@1 */
{ .vrefresh = 90, .frame_rate = 0x04, .e0 = 0x00 }, /* timing@2 */
{ .vrefresh = 48, .frame_rate = 0x05, .e0 = 0x00 }, /* timing@3 */
{ .vrefresh = 165, .frame_rate = 0x03, .e0 = 0x41 }, /* timing@4 */
};
/*
* Full on-command from 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)
static int nt37705_csot_on(struct nt37705_csot *ctx,
const struct nt37705_csot_rate *rate)
{
struct mipi_dsi_device *dsi = ctx->dsi;
struct mipi_dsi_multi_context dsi_ctx = { .dsi = dsi };
@@ -240,7 +264,7 @@ static int nt37705_csot_on(struct nt37705_csot *ctx)
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_var_seq_multi(&dsi_ctx, 0xe0, rate->e0);
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);
@@ -273,7 +297,7 @@ static int nt37705_csot_on(struct nt37705_csot *ctx)
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_var_seq_multi(&dsi_ctx, 0x2f, rate->frame_rate);
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);
@@ -327,6 +351,34 @@ static int nt37705_csot_off(struct nt37705_csot *ctx)
return dsi_ctx.accum_err;
}
/*
* drm_panel callbacks do not get the mode. prepare() runs from the bridge
* chain's pre_enable during an atomic commit, after the new states have
* been swapped in, so the connector passed to get_modes() leads to the mode
* being set. Anything unexpected falls back to 60 Hz.
*/
static const struct nt37705_csot_rate *
nt37705_csot_current_rate(struct nt37705_csot *ctx)
{
struct drm_connector *connector = ctx->connector;
struct drm_crtc *crtc;
int vrefresh, i;
if (!connector || !connector->state || !connector->state->crtc)
return &nt37705_csot_rates[0];
crtc = connector->state->crtc;
if (!crtc->state)
return &nt37705_csot_rates[0];
vrefresh = drm_mode_vrefresh(&crtc->state->mode);
for (i = 0; i < ARRAY_SIZE(nt37705_csot_rates); i++)
if (nt37705_csot_rates[i].vrefresh == vrefresh)
return &nt37705_csot_rates[i];
return &nt37705_csot_rates[0];
}
/*
* Everything is sent from prepare (DSI host already up thanks to
* prepare_prev_first), followed by the standard PPS and compression-mode
@@ -338,6 +390,7 @@ static int nt37705_csot_prepare(struct drm_panel *panel)
{
struct nt37705_csot *ctx = to_nt37705_csot(panel);
struct device *dev = &ctx->dsi->dev;
const struct nt37705_csot_rate *rate = nt37705_csot_current_rate(ctx);
struct drm_dsc_picture_parameter_set pps;
s64 elapsed;
int ret;
@@ -353,7 +406,9 @@ static int nt37705_csot_prepare(struct drm_panel *panel)
nt37705_csot_reset(ctx);
ret = nt37705_csot_on(ctx);
dev_info(dev, "on at %u Hz\n", rate->vrefresh);
ret = nt37705_csot_on(ctx, rate);
if (ret < 0) {
dev_err(dev, "failed to initialize panel: %d\n", ret);
goto err;
@@ -409,28 +464,56 @@ static int nt37705_csot_unprepare(struct drm_panel *panel)
}
/*
* 60 Hz modeline, geometry from the stock timing@0:
* Geometry from the stock timings, identical for every rate:
* 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,
#define NT37705_CSOT_MODE(hz) { \
.clock = (1080 + 32 + 16 + 32) * (2400 + 4 + 12 + 4) * (hz) / 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, \
}
/* Same order as nt37705_csot_rates[]; the first one is the preferred mode. */
static const struct drm_display_mode nt37705_csot_modes[] = {
NT37705_CSOT_MODE(60),
NT37705_CSOT_MODE(120),
NT37705_CSOT_MODE(90),
NT37705_CSOT_MODE(48),
NT37705_CSOT_MODE(165),
};
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);
struct nt37705_csot *ctx = to_nt37705_csot(panel);
struct drm_display_mode *mode;
int i;
for (i = 0; i < ARRAY_SIZE(nt37705_csot_modes); i++) {
mode = drm_mode_duplicate(connector->dev, &nt37705_csot_modes[i]);
if (!mode)
return -ENOMEM;
drm_mode_set_name(mode);
if (i == 0)
mode->type |= DRM_MODE_TYPE_PREFERRED;
drm_mode_probed_add(connector, mode);
}
connector->display_info.width_mm = nt37705_csot_modes[0].width_mm;
connector->display_info.height_mm = nt37705_csot_modes[0].height_mm;
ctx->connector = connector;
return ARRAY_SIZE(nt37705_csot_modes);
}
static const struct drm_panel_funcs nt37705_csot_panel_funcs = {