1a81b4fe6SChris Morgan // SPDX-License-Identifier: GPL-2.0 2a81b4fe6SChris Morgan /* 3a81b4fe6SChris Morgan * Driver for Anbernic panels with TD4310 panel controller. 4a81b4fe6SChris Morgan * 5a81b4fe6SChris Morgan * Copyright (C) 2026 Chris Morgan <macromorgan@hotmail.com> 6a81b4fe6SChris Morgan * 7a81b4fe6SChris Morgan */ 8a81b4fe6SChris Morgan 9a81b4fe6SChris Morgan #include <linux/gpio/consumer.h> 10a81b4fe6SChris Morgan #include <linux/module.h> 11a81b4fe6SChris Morgan #include <linux/of.h> 12a81b4fe6SChris Morgan #include <linux/regulator/consumer.h> 13a81b4fe6SChris Morgan 14a81b4fe6SChris Morgan #include <drm/drm_mipi_dsi.h> 15*b5a8b1f2SThierry Reding #include <drm/drm_of.h> 16a81b4fe6SChris Morgan #include <drm/drm_panel.h> 17a81b4fe6SChris Morgan #include <drm/drm_probe_helper.h> 18a81b4fe6SChris Morgan 19a81b4fe6SChris Morgan #include <video/mipi_display.h> 20a81b4fe6SChris Morgan 21a81b4fe6SChris Morgan struct anbernic_panel_td4310_info { 22a81b4fe6SChris Morgan const struct drm_display_mode *display_mode; 23a81b4fe6SChris Morgan u16 width_mm; 24a81b4fe6SChris Morgan u16 height_mm; 25a81b4fe6SChris Morgan u32 bus_flags; 26a81b4fe6SChris Morgan unsigned long mode_flags; 27a81b4fe6SChris Morgan u32 format; 28a81b4fe6SChris Morgan u32 lanes; 29a81b4fe6SChris Morgan u16 prepare_delay; 30a81b4fe6SChris Morgan u16 reset_delay; 31a81b4fe6SChris Morgan u16 init_delay; 32a81b4fe6SChris Morgan u16 enable_delay; 33a81b4fe6SChris Morgan u16 disable_delay; 34a81b4fe6SChris Morgan u16 unprepare_delay; 35a81b4fe6SChris Morgan }; 36a81b4fe6SChris Morgan 37a81b4fe6SChris Morgan struct anbernic_panel_td4310 { 38a81b4fe6SChris Morgan struct device *dev; 39a81b4fe6SChris Morgan struct mipi_dsi_device *dsi; 40a81b4fe6SChris Morgan struct drm_panel panel; 41a81b4fe6SChris Morgan const struct anbernic_panel_td4310_info *panel_info; 42a81b4fe6SChris Morgan struct gpio_desc *reset_gpio; 43a81b4fe6SChris Morgan struct gpio_desc *enable_gpio; 44a81b4fe6SChris Morgan struct regulator *vdd; 45a81b4fe6SChris Morgan enum drm_panel_orientation orientation; 46a81b4fe6SChris Morgan }; 47a81b4fe6SChris Morgan 48a81b4fe6SChris Morgan static inline struct anbernic_panel_td4310 *panel_to_anbernic_panel_td4310(struct drm_panel *panel) 49a81b4fe6SChris Morgan { 50a81b4fe6SChris Morgan return container_of(panel, struct anbernic_panel_td4310, panel); 51a81b4fe6SChris Morgan } 52a81b4fe6SChris Morgan 53a81b4fe6SChris Morgan static int panel_anbernic_td4310_prepare(struct drm_panel *panel) 54a81b4fe6SChris Morgan { 55a81b4fe6SChris Morgan struct anbernic_panel_td4310 *ctx = panel_to_anbernic_panel_td4310(panel); 56a81b4fe6SChris Morgan struct mipi_dsi_device *dsi = ctx->dsi; 57a81b4fe6SChris Morgan struct mipi_dsi_multi_context dsi_ctx = { .dsi = dsi }; 58a81b4fe6SChris Morgan int ret; 59a81b4fe6SChris Morgan 60a81b4fe6SChris Morgan ret = regulator_enable(ctx->vdd); 61a81b4fe6SChris Morgan if (ret) 62a81b4fe6SChris Morgan return ret; 63a81b4fe6SChris Morgan 64a81b4fe6SChris Morgan ret = gpiod_set_value_cansleep(ctx->enable_gpio, 1); 65a81b4fe6SChris Morgan if (ret) 66a81b4fe6SChris Morgan goto err_enable; 67a81b4fe6SChris Morgan 68a81b4fe6SChris Morgan if (ctx->panel_info->enable_delay) 69a81b4fe6SChris Morgan mipi_dsi_msleep(&dsi_ctx, ctx->panel_info->enable_delay); 70a81b4fe6SChris Morgan 71a81b4fe6SChris Morgan ret = gpiod_set_value_cansleep(ctx->reset_gpio, 1); 72a81b4fe6SChris Morgan if (ret) 73a81b4fe6SChris Morgan goto err_reset; 74a81b4fe6SChris Morgan 75a81b4fe6SChris Morgan mipi_dsi_msleep(&dsi_ctx, 10); 76a81b4fe6SChris Morgan 77a81b4fe6SChris Morgan ret = gpiod_set_value_cansleep(ctx->reset_gpio, 0); 78a81b4fe6SChris Morgan if (ret) 79a81b4fe6SChris Morgan goto err_reset; 80a81b4fe6SChris Morgan 81a81b4fe6SChris Morgan if (ctx->panel_info->reset_delay) 82a81b4fe6SChris Morgan mipi_dsi_msleep(&dsi_ctx, ctx->panel_info->reset_delay); 83a81b4fe6SChris Morgan 84a81b4fe6SChris Morgan mipi_dsi_dcs_exit_sleep_mode_multi(&dsi_ctx); 85a81b4fe6SChris Morgan mipi_dsi_msleep(&dsi_ctx, ctx->panel_info->prepare_delay); 86a81b4fe6SChris Morgan mipi_dsi_dcs_set_display_on_multi(&dsi_ctx); 87a81b4fe6SChris Morgan mipi_dsi_msleep(&dsi_ctx, ctx->panel_info->prepare_delay); 88a81b4fe6SChris Morgan 89a81b4fe6SChris Morgan if (dsi_ctx.accum_err) { 90a81b4fe6SChris Morgan ret = dsi_ctx.accum_err; 91a81b4fe6SChris Morgan goto err_reset; 92a81b4fe6SChris Morgan } 93a81b4fe6SChris Morgan 94a81b4fe6SChris Morgan return 0; 95a81b4fe6SChris Morgan 96a81b4fe6SChris Morgan err_reset: 97a81b4fe6SChris Morgan gpiod_set_value_cansleep(ctx->enable_gpio, 0); 98a81b4fe6SChris Morgan err_enable: 99a81b4fe6SChris Morgan regulator_disable(ctx->vdd); 100a81b4fe6SChris Morgan return ret; 101a81b4fe6SChris Morgan } 102a81b4fe6SChris Morgan 103a81b4fe6SChris Morgan static int panel_anbernic_td4310_unprepare(struct drm_panel *panel) 104a81b4fe6SChris Morgan { 105a81b4fe6SChris Morgan struct anbernic_panel_td4310 *ctx = panel_to_anbernic_panel_td4310(panel); 106a81b4fe6SChris Morgan struct mipi_dsi_device *dsi = ctx->dsi; 107a81b4fe6SChris Morgan struct mipi_dsi_multi_context dsi_ctx = { .dsi = dsi }; 108a81b4fe6SChris Morgan 109a81b4fe6SChris Morgan mipi_dsi_dcs_set_display_off_multi(&dsi_ctx); 110a81b4fe6SChris Morgan mipi_dsi_msleep(&dsi_ctx, ctx->panel_info->unprepare_delay); 111a81b4fe6SChris Morgan mipi_dsi_dcs_enter_sleep_mode_multi(&dsi_ctx); 112a81b4fe6SChris Morgan mipi_dsi_msleep(&dsi_ctx, ctx->panel_info->disable_delay); 113a81b4fe6SChris Morgan 114a81b4fe6SChris Morgan gpiod_set_value_cansleep(ctx->enable_gpio, 0); 115a81b4fe6SChris Morgan 116a81b4fe6SChris Morgan gpiod_set_value_cansleep(ctx->reset_gpio, 1); 117a81b4fe6SChris Morgan 118a81b4fe6SChris Morgan regulator_disable(ctx->vdd); 119a81b4fe6SChris Morgan 120a81b4fe6SChris Morgan return 0; 121a81b4fe6SChris Morgan } 122a81b4fe6SChris Morgan 123a81b4fe6SChris Morgan static int panel_anbernic_td4310_get_mode(struct drm_panel *panel, 124a81b4fe6SChris Morgan struct drm_connector *connector) 125a81b4fe6SChris Morgan { 126a81b4fe6SChris Morgan struct anbernic_panel_td4310 *ctx = panel_to_anbernic_panel_td4310(panel); 127a81b4fe6SChris Morgan const struct anbernic_panel_td4310_info *panel_info = ctx->panel_info; 128a81b4fe6SChris Morgan 129a81b4fe6SChris Morgan connector->display_info.bpc = 8; 130a81b4fe6SChris Morgan connector->display_info.width_mm = panel_info->width_mm; 131a81b4fe6SChris Morgan connector->display_info.height_mm = panel_info->height_mm; 132a81b4fe6SChris Morgan connector->display_info.bus_flags = panel_info->bus_flags; 133a81b4fe6SChris Morgan 134a81b4fe6SChris Morgan return drm_connector_helper_get_modes_fixed(connector, panel_info->display_mode); 135a81b4fe6SChris Morgan } 136a81b4fe6SChris Morgan 137a81b4fe6SChris Morgan static enum drm_panel_orientation panel_anbernic_td4310_get_orientation(struct drm_panel *panel) 138a81b4fe6SChris Morgan { 139a81b4fe6SChris Morgan struct anbernic_panel_td4310 *ctx = panel_to_anbernic_panel_td4310(panel); 140a81b4fe6SChris Morgan 141a81b4fe6SChris Morgan return ctx->orientation; 142a81b4fe6SChris Morgan } 143a81b4fe6SChris Morgan 144a81b4fe6SChris Morgan static const struct drm_panel_funcs panel_anbernic_td4310_funcs = { 145a81b4fe6SChris Morgan .prepare = panel_anbernic_td4310_prepare, 146a81b4fe6SChris Morgan .unprepare = panel_anbernic_td4310_unprepare, 147a81b4fe6SChris Morgan .get_modes = panel_anbernic_td4310_get_mode, 148a81b4fe6SChris Morgan .get_orientation = panel_anbernic_td4310_get_orientation, 149a81b4fe6SChris Morgan }; 150a81b4fe6SChris Morgan 151a81b4fe6SChris Morgan static int panel_anbernic_td4310_probe(struct mipi_dsi_device *dsi) 152a81b4fe6SChris Morgan { 153a81b4fe6SChris Morgan struct device *dev = &dsi->dev; 154a81b4fe6SChris Morgan struct anbernic_panel_td4310 *ctx; 155a81b4fe6SChris Morgan int ret; 156a81b4fe6SChris Morgan 157a81b4fe6SChris Morgan ctx = devm_drm_panel_alloc(dev, struct anbernic_panel_td4310, panel, 158a81b4fe6SChris Morgan &panel_anbernic_td4310_funcs, 159a81b4fe6SChris Morgan DRM_MODE_CONNECTOR_DSI); 160a81b4fe6SChris Morgan if (IS_ERR(ctx)) 161a81b4fe6SChris Morgan return PTR_ERR(ctx); 162a81b4fe6SChris Morgan 163a81b4fe6SChris Morgan ctx->dev = dev; 164a81b4fe6SChris Morgan 165a81b4fe6SChris Morgan ctx->panel_info = of_device_get_match_data(dev); 166a81b4fe6SChris Morgan if (!ctx->panel_info) 167a81b4fe6SChris Morgan return -EINVAL; 168a81b4fe6SChris Morgan 169*b5a8b1f2SThierry Reding ret = drm_of_get_panel_orientation(dev->of_node, &ctx->orientation); 170a81b4fe6SChris Morgan if (ret < 0) 171a81b4fe6SChris Morgan return dev_err_probe(dev, ret, "Failed to get panel orientation\n"); 172a81b4fe6SChris Morgan 173a81b4fe6SChris Morgan ctx->reset_gpio = devm_gpiod_get(dev, "reset", GPIOD_OUT_LOW); 174a81b4fe6SChris Morgan if (IS_ERR(ctx->reset_gpio)) 175a81b4fe6SChris Morgan return dev_err_probe(dev, PTR_ERR(ctx->reset_gpio), 176a81b4fe6SChris Morgan "Cannot get reset gpio\n"); 177a81b4fe6SChris Morgan 178a81b4fe6SChris Morgan ctx->enable_gpio = devm_gpiod_get_optional(dev, "enable", GPIOD_OUT_LOW); 179a81b4fe6SChris Morgan if (IS_ERR(ctx->enable_gpio)) 180a81b4fe6SChris Morgan return dev_err_probe(dev, PTR_ERR(ctx->enable_gpio), 181a81b4fe6SChris Morgan "Cannot get enable gpio\n"); 182a81b4fe6SChris Morgan 183a81b4fe6SChris Morgan ctx->vdd = devm_regulator_get(dev, "vdd"); 184a81b4fe6SChris Morgan if (IS_ERR(ctx->vdd)) 185a81b4fe6SChris Morgan return dev_err_probe(dev, PTR_ERR(ctx->vdd), 186a81b4fe6SChris Morgan "Failed to request vdd regulator\n"); 187a81b4fe6SChris Morgan 188a81b4fe6SChris Morgan ctx->dsi = dsi; 189a81b4fe6SChris Morgan mipi_dsi_set_drvdata(dsi, ctx); 190a81b4fe6SChris Morgan 191a81b4fe6SChris Morgan dsi->lanes = ctx->panel_info->lanes; 192a81b4fe6SChris Morgan dsi->format = ctx->panel_info->format; 193a81b4fe6SChris Morgan dsi->mode_flags = ctx->panel_info->mode_flags; 194a81b4fe6SChris Morgan 195a81b4fe6SChris Morgan ret = drm_panel_of_backlight(&ctx->panel); 196a81b4fe6SChris Morgan if (ret) 197a81b4fe6SChris Morgan return ret; 198a81b4fe6SChris Morgan 199a81b4fe6SChris Morgan devm_drm_panel_add(dev, &ctx->panel); 200a81b4fe6SChris Morgan 201a81b4fe6SChris Morgan ret = devm_mipi_dsi_attach(dev, dsi); 202a81b4fe6SChris Morgan if (ret < 0) 203a81b4fe6SChris Morgan return dev_err_probe(dev, ret, "Failed to attach to DSI host\n"); 204a81b4fe6SChris Morgan 205a81b4fe6SChris Morgan return 0; 206a81b4fe6SChris Morgan } 207a81b4fe6SChris Morgan 208a81b4fe6SChris Morgan static const struct drm_display_mode anbernic_vitapro_mode = { 209a81b4fe6SChris Morgan .clock = 140020, 210a81b4fe6SChris Morgan .hdisplay = 1080, 211a81b4fe6SChris Morgan .hsync_start = 1080 + 50, 212a81b4fe6SChris Morgan .hsync_end = 1080 + 50 + 4, 213a81b4fe6SChris Morgan .htotal = 1080 + 50 + 4 + 50, 214a81b4fe6SChris Morgan .vdisplay = 1920, 215a81b4fe6SChris Morgan .vsync_start = 1920 + 15, 216a81b4fe6SChris Morgan .vsync_end = 1920 + 15 + 4, 217a81b4fe6SChris Morgan .vtotal = 1920 + 15 + 4 + 32, 218a81b4fe6SChris Morgan .flags = DRM_MODE_FLAG_NHSYNC | DRM_MODE_FLAG_NVSYNC, 219a81b4fe6SChris Morgan }; 220a81b4fe6SChris Morgan 221a81b4fe6SChris Morgan static const struct anbernic_panel_td4310_info anbernic_vitapro_info = { 222a81b4fe6SChris Morgan .display_mode = &anbernic_vitapro_mode, 223a81b4fe6SChris Morgan .width_mm = 69, 224a81b4fe6SChris Morgan .height_mm = 121, 225a81b4fe6SChris Morgan .bus_flags = DRM_BUS_FLAG_DE_LOW | DRM_BUS_FLAG_PIXDATA_DRIVE_NEGEDGE, 226a81b4fe6SChris Morgan .mode_flags = MIPI_DSI_MODE_VIDEO | MIPI_DSI_MODE_VIDEO_BURST | 227a81b4fe6SChris Morgan MIPI_DSI_MODE_LPM | MIPI_DSI_MODE_NO_EOT_PACKET | 228a81b4fe6SChris Morgan MIPI_DSI_CLOCK_NON_CONTINUOUS, 229a81b4fe6SChris Morgan .format = MIPI_DSI_FMT_RGB888, 230a81b4fe6SChris Morgan .lanes = 4, 231a81b4fe6SChris Morgan .prepare_delay = 50, 232a81b4fe6SChris Morgan .reset_delay = 220, 233a81b4fe6SChris Morgan .enable_delay = 120, 234a81b4fe6SChris Morgan .disable_delay = 50, 235a81b4fe6SChris Morgan .unprepare_delay = 20, 236a81b4fe6SChris Morgan }; 237a81b4fe6SChris Morgan 238a81b4fe6SChris Morgan static const struct of_device_id panel_anbernic_td4310_of_match[] = { 239a81b4fe6SChris Morgan { 240a81b4fe6SChris Morgan .compatible = "anbernic,panel-vita-pro", 241a81b4fe6SChris Morgan .data = &anbernic_vitapro_info, 242a81b4fe6SChris Morgan }, 243a81b4fe6SChris Morgan { } 244a81b4fe6SChris Morgan }; 245a81b4fe6SChris Morgan MODULE_DEVICE_TABLE(of, panel_anbernic_td4310_of_match); 246a81b4fe6SChris Morgan 247a81b4fe6SChris Morgan static struct mipi_dsi_driver anbernic_panel_td4310_driver = { 248a81b4fe6SChris Morgan .driver = { 249a81b4fe6SChris Morgan .name = "panel-anbernic-td4310", 250a81b4fe6SChris Morgan .of_match_table = panel_anbernic_td4310_of_match, 251a81b4fe6SChris Morgan }, 252a81b4fe6SChris Morgan .probe = panel_anbernic_td4310_probe, 253a81b4fe6SChris Morgan }; 254a81b4fe6SChris Morgan module_mipi_dsi_driver(anbernic_panel_td4310_driver); 255a81b4fe6SChris Morgan 256a81b4fe6SChris Morgan MODULE_AUTHOR("Chris Morgan <macromorgan@hotmail.com>"); 257a81b4fe6SChris Morgan MODULE_DESCRIPTION("DRM driver for Anbernic TD4310 MIPI DSI panels"); 258a81b4fe6SChris Morgan MODULE_LICENSE("GPL"); 259