Message ID | 20190109135331.14895-1-linus.walleij@linaro.org (mailing list archive) |
---|---|
State | New, archived |
Headers | show |
Series | [v6] drm/panel: Add a driver for the TPO TPG110 | expand |
Hi Linus. On Wed, Jan 09, 2019 at 02:53:31PM +0100, Linus Walleij wrote: > The TPO (Toppoly) TPG110 is a pretty generic display driver > similar in vein to the Ilitek 93xx devices. It is not a panel > per se but a driver used with several low-cost noname panels. > > This is used on the Nomadik NHK15 combined with a OSD > OSD057VA01CT display for WVGA 800x480. > > The driver is pretty minimalistic right now but can be > extended to handle non-default polarities, gamma correction > etc. > > The driver is based on the baked-in code in > drivers/video/fbdev/amba-clcd-nomadik.c which will be > decomissioned once this us upstream. > > Acked-by: Sam Ravnborg <sam@ravnborg.org> > Signed-off-by: Linus Walleij <linus.walleij@linaro.org> While browsing this code again a few things jumped at me. - we try to kill use of drmP.h - DRM_DEV_XXX rather than dev_xxx (not all like these, but they are used in other panels) - drop videomode - not used anymore. I went ahead and made a patch. Build tested on top of drm-misc-next (had to define SPI_3WIRE_HIZ, because we miss a backmerge of -rc1) Apply what you think is relevant, and sorry for the late feedback. Sam diff --git a/drivers/gpu/drm/panel/Kconfig b/drivers/gpu/drm/panel/Kconfig index e3821180b6cd..a71f44191273 100644 --- a/drivers/gpu/drm/panel/Kconfig +++ b/drivers/gpu/drm/panel/Kconfig @@ -208,7 +208,6 @@ config DRM_PANEL_TPO_TPG110 tristate "TPO TPG 800x400 panel" depends on OF && SPI && GPIOLIB depends on BACKLIGHT_CLASS_DEVICE - select VIDEOMODE_HELPERS help Say Y here if you want to enable support for TPO TPG110 400CH LTPS TFT LCD Single Chip Digital Driver for up to diff --git a/drivers/gpu/drm/panel/panel-tpo-tpg110.c b/drivers/gpu/drm/panel/panel-tpo-tpg110.c index d7c3541fdf28..67c7be8eb166 100644 --- a/drivers/gpu/drm/panel/panel-tpo-tpg110.c +++ b/drivers/gpu/drm/panel/panel-tpo-tpg110.c @@ -10,11 +10,14 @@ * Author: * Linus Walleij <linus.walleij@linaro.org> */ -#include <drm/drmP.h> + +#include <drm/drm_modes.h> #include <drm/drm_panel.h> +#include <drm/drm_print.h> #include <linux/backlight.h> #include <linux/bitops.h> +#include <linux/delay.h> #include <linux/gpio/consumer.h> #include <linux/init.h> #include <linux/kernel.h> @@ -22,9 +25,6 @@ #include <linux/platform_device.h> #include <linux/spi/spi.h> -#include <video/of_videomode.h> -#include <video/videomode.h> - #define TPG110_TEST 0x00 #define TPG110_CHIPID 0x01 #define TPG110_CTRL1 0x02 @@ -248,7 +248,7 @@ static u8 tpg110_readwrite_reg(struct tpg110 *tpg, bool write, spi_message_add_tail(&t[1], &m); ret = spi_sync(tpg->spi, &m); if (ret) { - dev_err(tpg->dev, "SPI message error %d\n", ret); + DRM_DEV_ERROR(tpg->dev, "SPI message error %d\n", ret); return ret; } if (write) @@ -275,46 +275,46 @@ static int tpg110_startup(struct tpg110 *tpg) /* De-assert the reset signal */ gpiod_set_value_cansleep(tpg->grestb, 0); mdelay(1); - dev_info(tpg->dev, "de-asserted GRESTB\n"); + DRM_DEV_INFO(tpg->dev, "de-asserted GRESTB\n"); /* Test display communication */ tpg110_write_reg(tpg, TPG110_TEST, 0x55); val = tpg110_read_reg(tpg, TPG110_TEST); if (val != 0x55) { - dev_err(tpg->dev, "failed communication test\n"); + DRM_DEV_ERROR(tpg->dev, "failed communication test\n"); return -ENODEV; } val = tpg110_read_reg(tpg, TPG110_CHIPID); - dev_info(tpg->dev, "TPG110 chip ID: %d version: %d\n", - val >> 4, val & 0x0f); + DRM_DEV_INFO(tpg->dev, "TPG110 chip ID: %d version: %d\n", + val >> 4, val & 0x0f); /* Show display resolution */ val = tpg110_read_reg(tpg, TPG110_CTRL1); val &= TPG110_RES_MASK; switch (val) { case TPG110_RES_400X240_D: - dev_info(tpg->dev, - "IN 400x240 RGB -> OUT 800x480 RGB (dual scan)"); + DRM_DEV_INFO(tpg->dev, + "IN 400x240 RGB -> OUT 800x480 RGB (dual scan)"); break; case TPG110_RES_480X272_D: - dev_info(tpg->dev, - "IN 480x272 RGB -> OUT 800x480 RGB (dual scan)"); + DRM_DEV_INFO(tpg->dev, + "IN 480x272 RGB -> OUT 800x480 RGB (dual scan)"); break; case TPG110_RES_480X640: - dev_info(tpg->dev, "480x640 RGB"); + DRM_DEV_INFO(tpg->dev, "480x640 RGB"); break; case TPG110_RES_480X272: - dev_info(tpg->dev, "480x272 RGB"); + DRM_DEV_INFO(tpg->dev, "480x272 RGB"); break; case TPG110_RES_640X480: - dev_info(tpg->dev, "640x480 RGB"); + DRM_DEV_INFO(tpg->dev, "640x480 RGB"); break; case TPG110_RES_800X480: - dev_info(tpg->dev, "800x480 RGB"); + DRM_DEV_INFO(tpg->dev, "800x480 RGB"); break; default: - dev_info(tpg->dev, "ILLEGAL RESOLUTION"); + DRM_DEV_INFO(tpg->dev, "ILLEGAL RESOLUTION"); break; } @@ -332,13 +332,13 @@ static int tpg110_startup(struct tpg110 *tpg) } } if (i == ARRAY_SIZE(tpg110_modes)) { - dev_err(tpg->dev, "unsupported mode (%02x) detected\n", + DRM_DEV_ERROR(tpg->dev, "unsupported mode (%02x) detected\n", val); return -ENODEV; } val = tpg110_read_reg(tpg, TPG110_CTRL2); - dev_info(tpg->dev, "resolution and standby is controlled by %s\n", + DRM_DEV_INFO(tpg->dev, "resolution and standby is controlled by %s\n", (val & TPG110_CTRL2_RES_PM_CTRL) ? "software" : "hardware"); /* Take control over resolution and standby */ val |= TPG110_CTRL2_RES_PM_CTRL; @@ -439,10 +439,10 @@ static int tpg110_probe(struct spi_device *spi) /* We get the physical display dimensions from the DT */ ret = of_property_read_u32(np, "width-mm", &tpg->width); if (ret) - dev_err(dev, "no panel width specified\n"); + DRM_DEV_ERROR(dev, "no panel width specified\n"); ret = of_property_read_u32(np, "height-mm", &tpg->height); if (ret) - dev_err(dev, "no panel height specified\n"); + DRM_DEV_ERROR(dev, "no panel height specified\n"); /* Look for some optional backlight */ backlight = of_parse_phandle(np, "backlight", 0); @@ -457,7 +457,7 @@ static int tpg110_probe(struct spi_device *spi) /* This asserts the GRESTB signal, putting the display into reset */ tpg->grestb = devm_gpiod_get(dev, "grestb", GPIOD_OUT_HIGH); if (IS_ERR(tpg->grestb)) { - dev_err(dev, "no GRESTB GPIO\n"); + DRM_DEV_ERROR(dev, "no GRESTB GPIO\n"); return -ENODEV; } @@ -465,7 +465,7 @@ static int tpg110_probe(struct spi_device *spi) spi->mode |= SPI_3WIRE_HIZ; ret = spi_setup(spi); if (ret < 0) { - dev_err(dev, "spi setup failed.\n"); + DRM_DEV_ERROR(dev, "spi setup failed.\n"); return ret; } tpg->spi = spi;
On Wed, Jan 09, 2019 at 02:53:31PM +0100, Linus Walleij wrote: [...] > diff --git a/drivers/gpu/drm/panel/panel-tpo-tpg110.c b/drivers/gpu/drm/panel/panel-tpo-tpg110.c [...] > +static int tpg110_startup(struct tpg110 *tpg) > +{ > + u8 val; > + int i; > + > + /* De-assert the reset signal */ > + gpiod_set_value_cansleep(tpg->grestb, 0); > + mdelay(1); Does this have to be the spinning variant? This seems to be only used in the probe path, so doesn't have to be atomic. > + dev_info(tpg->dev, "de-asserted GRESTB\n"); Maybe turn this into dev_dbg()? > + > + /* Test display communication */ > + tpg110_write_reg(tpg, TPG110_TEST, 0x55); > + val = tpg110_read_reg(tpg, TPG110_TEST); > + if (val != 0x55) { > + dev_err(tpg->dev, "failed communication test\n"); > + return -ENODEV; > + } > + > + val = tpg110_read_reg(tpg, TPG110_CHIPID); > + dev_info(tpg->dev, "TPG110 chip ID: %d version: %d\n", > + val >> 4, val & 0x0f); > + > + /* Show display resolution */ > + val = tpg110_read_reg(tpg, TPG110_CTRL1); > + val &= TPG110_RES_MASK; > + switch (val) { > + case TPG110_RES_400X240_D: > + dev_info(tpg->dev, > + "IN 400x240 RGB -> OUT 800x480 RGB (dual scan)"); > + break; > + case TPG110_RES_480X272_D: > + dev_info(tpg->dev, > + "IN 480x272 RGB -> OUT 800x480 RGB (dual scan)"); > + break; > + case TPG110_RES_480X640: > + dev_info(tpg->dev, "480x640 RGB"); > + break; > + case TPG110_RES_480X272: > + dev_info(tpg->dev, "480x272 RGB"); > + break; > + case TPG110_RES_640X480: > + dev_info(tpg->dev, "640x480 RGB"); > + break; > + case TPG110_RES_800X480: > + dev_info(tpg->dev, "800x480 RGB"); > + break; > + default: > + dev_info(tpg->dev, "ILLEGAL RESOLUTION"); dev_err()? Also, perhaps make this some proper error message and include the invalid value? Also I just noticed that the above informational messages all lack a \n at the end. The above is also very verbose, do we really need all that information in the kernel log? > + break; > + } > + > + /* From the producer side, this is the same resolution */ > + if (val == TPG110_RES_480X272_D) > + val = TPG110_RES_480X272; > + > + for (i = 0; i < ARRAY_SIZE(tpg110_modes); i++) { > + const struct tpg110_panel_mode *pm; > + > + pm = &tpg110_modes[i]; > + if (pm->magic == val) { > + tpg->panel_mode = pm; > + break; > + } > + } > + if (i == ARRAY_SIZE(tpg110_modes)) { > + dev_err(tpg->dev, "unsupported mode (%02x) detected\n", > + val); > + return -ENODEV; > + } > + > + val = tpg110_read_reg(tpg, TPG110_CTRL2); > + dev_info(tpg->dev, "resolution and standby is controlled by %s\n", > + (val & TPG110_CTRL2_RES_PM_CTRL) ? "software" : "hardware"); > + /* Take control over resolution and standby */ > + val |= TPG110_CTRL2_RES_PM_CTRL; > + tpg110_write_reg(tpg, TPG110_CTRL2, val); > + > + return 0; > +} > + > +static int tpg110_disable(struct drm_panel *panel) > +{ > + struct tpg110 *tpg = to_tpg110(panel); > + u8 val; > + > + /* Put chip into standby */ > + val = tpg110_read_reg(tpg, TPG110_CTRL2_PM); > + val &= ~TPG110_CTRL2_PM; > + tpg110_write_reg(tpg, TPG110_CTRL2_PM, val); > + > + if (tpg->backlight) { > + tpg->backlight->props.power = FB_BLANK_POWERDOWN; > + tpg->backlight->props.state |= BL_CORE_FBBLANK; > + backlight_update_status(tpg->backlight); > + } backlight_disable()? > + > + return 0; > +} > + > +static int tpg110_enable(struct drm_panel *panel) > +{ > + struct tpg110 *tpg = to_tpg110(panel); > + u8 val; > + > + if (tpg->backlight) { > + tpg->backlight->props.state &= ~BL_CORE_FBBLANK; > + tpg->backlight->props.power = FB_BLANK_UNBLANK; > + backlight_update_status(tpg->backlight); > + } backlight_enable()? Thierry
Hi Thierry. > > + case TPG110_RES_480X272: > > + dev_info(tpg->dev, "480x272 RGB"); > > + break; > > + case TPG110_RES_640X480: > > + dev_info(tpg->dev, "640x480 RGB"); > > + break; > > + case TPG110_RES_800X480: > > + dev_info(tpg->dev, "800x480 RGB"); > > + break; > > + default: > > + dev_info(tpg->dev, "ILLEGAL RESOLUTION"); > > dev_err()? Also, perhaps make this some proper error message and include > the invalid value? > > Also I just noticed that the above informational messages all lack a \n > at the end. The "\n" is optional these days. Sam
Hi Linus, On Wed, Jan 9, 2019 at 7:23 PM Linus Walleij <linus.walleij@linaro.org> wrote: > > The TPO (Toppoly) TPG110 is a pretty generic display driver > similar in vein to the Ilitek 93xx devices. It is not a panel > per se but a driver used with several low-cost noname panels. > > This is used on the Nomadik NHK15 combined with a OSD > OSD057VA01CT display for WVGA 800x480. > > The driver is pretty minimalistic right now but can be > extended to handle non-default polarities, gamma correction > etc. > > The driver is based on the baked-in code in > drivers/video/fbdev/amba-clcd-nomadik.c which will be > decomissioned once this us upstream. > > Acked-by: Sam Ravnborg <sam@ravnborg.org> > Signed-off-by: Linus Walleij <linus.walleij@linaro.org> > --- > ChangeLog v5->v6: > - Collected Sam's ACK. > ChangeLog v4->v5: > - Assign proper bus_flags. > - This is now the only remaining patch. > ChangeLog v3->v4: > - Tag on the SPI_3WIRE_HIZ flag to the SPI slave mode. > ChangeLog v2->v3: > - Rewrite as an SPI child device. > --- > MAINTAINERS | 7 + > drivers/gpu/drm/panel/Kconfig | 10 + > drivers/gpu/drm/panel/Makefile | 1 + > drivers/gpu/drm/panel/panel-tpo-tpg110.c | 511 +++++++++++++++++++++++ > 4 files changed, 529 insertions(+) > create mode 100644 drivers/gpu/drm/panel/panel-tpo-tpg110.c > > diff --git a/MAINTAINERS b/MAINTAINERS > index 32d444476a90..e177473d5417 100644 > --- a/MAINTAINERS > +++ b/MAINTAINERS > @@ -4909,6 +4909,13 @@ DRM DRIVER FOR TDFX VIDEO CARDS > S: Orphan / Obsolete > F: drivers/gpu/drm/tdfx/ > > +DRM DRIVER FOR TPO TPG110 PANELS > +M: Linus Walleij <linus.walleij@linaro.org> > +T: git git://anongit.freedesktop.org/drm/drm-misc > +S: Maintained > +F: drivers/gpu/drm/panel/panel-tpo-tpg110.c > +F: Documentation/devicetree/bindings/display/panel/tpo,tpg110.txt Just for my information, for each new panel addition, is MAINTAINERS file will update only by drm-misc members or anyone who submitted the patch? The last communication with seanpaul mentioned this context[1] just want to understand how this panels were maintained. [1] https://lkml.org/lkml/2018/12/14/1275
On Thu, Jan 10, 2019 at 9:01 PM Jagan Teki <jagan@amarulasolutions.com> wrote: > Just for my information, for each new panel addition, is MAINTAINERS > file will update only by drm-misc members or anyone who submitted the > patch? The last communication with seanpaul mentioned this context[1] > just want to understand how this panels were maintained. > > [1] https://lkml.org/lkml/2018/12/14/1275 Aha I see. Well this per-panel entry is good because then get_maintainer.pl will also get me specifically and not just the people listed for panel/drm-misc (whether I am, was or will be a member of that group or not). get_maintainer.pl will present all of them with me on top: $ scripts/get_maintainer.pl -f drivers/gpu/drm/panel/panel-tpo-tpg110.c Linus Walleij <linus.walleij@linaro.org> (maintainer:DRM DRIVER FOR TPO TPG110 PANELS) Thierry Reding <thierry.reding@gmail.com> (maintainer:DRM PANEL DRIVERS) David Airlie <airlied@linux.ie> (maintainer:DRM DRIVERS) Daniel Vetter <daniel@ffwll.ch> (maintainer:DRM DRIVERS) dri-devel@lists.freedesktop.org (open list:DRM PANEL DRIVERS) linux-kernel@vger.kernel.org (open list) So I would advice against the above practice: one does not exclude the other. Yours, Linus Walleij
On Fri, Jan 11, 2019 at 8:54 PM Linus Walleij <linus.walleij@linaro.org> wrote: > > On Thu, Jan 10, 2019 at 9:01 PM Jagan Teki <jagan@amarulasolutions.com> wrote: > > > Just for my information, for each new panel addition, is MAINTAINERS > > file will update only by drm-misc members or anyone who submitted the > > patch? The last communication with seanpaul mentioned this context[1] > > just want to understand how this panels were maintained. > > > > [1] https://lkml.org/lkml/2018/12/14/1275 > > Aha I see. Well this per-panel entry is good because then > get_maintainer.pl will also get me specifically and not just > the people listed for panel/drm-misc (whether I am, was > or will be a member of that group or not). > > get_maintainer.pl will present all of them with me on > top: > > $ scripts/get_maintainer.pl -f drivers/gpu/drm/panel/panel-tpo-tpg110.c > Linus Walleij <linus.walleij@linaro.org> (maintainer:DRM DRIVER FOR > TPO TPG110 PANELS) > Thierry Reding <thierry.reding@gmail.com> (maintainer:DRM PANEL DRIVERS) > David Airlie <airlied@linux.ie> (maintainer:DRM DRIVERS) > Daniel Vetter <daniel@ffwll.ch> (maintainer:DRM DRIVERS) > dri-devel@lists.freedesktop.org (open list:DRM PANEL DRIVERS) > linux-kernel@vger.kernel.org (open list) > > So I would advice against the above practice: one does not > exclude the other. Yeah we have lots of MAINTAINERS entries for all the various things maintained as part of drm-misc. Only thing I'm encouraging is that everything has at least a T: git repo entry pointing at drm-misc somewhere, to have some easy overview of what's under the drm-misc umbrella. But the overall drm panel entry has that covered already. And we also have some other more specific entries for individual panels already. -Daniel > > Yours, > Linus Walleij > _______________________________________________ > dri-devel mailing list > dri-devel@lists.freedesktop.org > https://lists.freedesktop.org/mailman/listinfo/dri-devel
diff --git a/MAINTAINERS b/MAINTAINERS index 32d444476a90..e177473d5417 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -4909,6 +4909,13 @@ DRM DRIVER FOR TDFX VIDEO CARDS S: Orphan / Obsolete F: drivers/gpu/drm/tdfx/ +DRM DRIVER FOR TPO TPG110 PANELS +M: Linus Walleij <linus.walleij@linaro.org> +T: git git://anongit.freedesktop.org/drm/drm-misc +S: Maintained +F: drivers/gpu/drm/panel/panel-tpo-tpg110.c +F: Documentation/devicetree/bindings/display/panel/tpo,tpg110.txt + DRM DRIVER FOR USB DISPLAYLINK VIDEO ADAPTERS M: Dave Airlie <airlied@redhat.com> R: Sean Paul <sean@poorly.run> diff --git a/drivers/gpu/drm/panel/Kconfig b/drivers/gpu/drm/panel/Kconfig index 3f3537719beb..e3821180b6cd 100644 --- a/drivers/gpu/drm/panel/Kconfig +++ b/drivers/gpu/drm/panel/Kconfig @@ -204,6 +204,16 @@ config DRM_PANEL_SITRONIX_ST7789V Say Y here if you want to enable support for the Sitronix ST7789V controller for 240x320 LCD panels +config DRM_PANEL_TPO_TPG110 + tristate "TPO TPG 800x400 panel" + depends on OF && SPI && GPIOLIB + depends on BACKLIGHT_CLASS_DEVICE + select VIDEOMODE_HELPERS + help + Say Y here if you want to enable support for TPO TPG110 + 400CH LTPS TFT LCD Single Chip Digital Driver for up to + 800x400 LCD panels. + config DRM_PANEL_TRULY_NT35597_WQXGA tristate "Truly WQXGA" depends on OF diff --git a/drivers/gpu/drm/panel/Makefile b/drivers/gpu/drm/panel/Makefile index 4396658a7996..cd14ca39c6e0 100644 --- a/drivers/gpu/drm/panel/Makefile +++ b/drivers/gpu/drm/panel/Makefile @@ -21,4 +21,5 @@ obj-$(CONFIG_DRM_PANEL_SEIKO_43WVF1G) += panel-seiko-43wvf1g.o obj-$(CONFIG_DRM_PANEL_SHARP_LQ101R1SX01) += panel-sharp-lq101r1sx01.o obj-$(CONFIG_DRM_PANEL_SHARP_LS043T1LE01) += panel-sharp-ls043t1le01.o obj-$(CONFIG_DRM_PANEL_SITRONIX_ST7789V) += panel-sitronix-st7789v.o +obj-$(CONFIG_DRM_PANEL_TPO_TPG110) += panel-tpo-tpg110.o obj-$(CONFIG_DRM_PANEL_TRULY_NT35597_WQXGA) += panel-truly-nt35597.o diff --git a/drivers/gpu/drm/panel/panel-tpo-tpg110.c b/drivers/gpu/drm/panel/panel-tpo-tpg110.c new file mode 100644 index 000000000000..d7c3541fdf28 --- /dev/null +++ b/drivers/gpu/drm/panel/panel-tpo-tpg110.c @@ -0,0 +1,511 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Panel driver for the TPO TPG110 400CH LTPS TFT LCD Single Chip + * Digital Driver. + * + * This chip drives a TFT LCD, so it does not know what kind of + * display is actually connected to it, so the width and height of that + * display needs to be supplied from the machine configuration. + * + * Author: + * Linus Walleij <linus.walleij@linaro.org> + */ +#include <drm/drmP.h> +#include <drm/drm_panel.h> + +#include <linux/backlight.h> +#include <linux/bitops.h> +#include <linux/gpio/consumer.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/spi/spi.h> + +#include <video/of_videomode.h> +#include <video/videomode.h> + +#define TPG110_TEST 0x00 +#define TPG110_CHIPID 0x01 +#define TPG110_CTRL1 0x02 +#define TPG110_RES_MASK GENMASK(2, 0) +#define TPG110_RES_800X480 0x07 +#define TPG110_RES_640X480 0x06 +#define TPG110_RES_480X272 0x05 +#define TPG110_RES_480X640 0x04 +#define TPG110_RES_480X272_D 0x01 /* Dual scan: outputs 800x480 */ +#define TPG110_RES_400X240_D 0x00 /* Dual scan: outputs 800x480 */ +#define TPG110_CTRL2 0x03 +#define TPG110_CTRL2_PM BIT(0) +#define TPG110_CTRL2_RES_PM_CTRL BIT(7) + +/** + * struct tpg110_panel_mode - lookup struct for the supported modes + */ +struct tpg110_panel_mode { + /** + * @name: the name of this panel + */ + const char *name; + /** + * @magic: the magic value from the detection register + */ + u32 magic; + /** + * @mode: the DRM display mode for this panel + */ + struct drm_display_mode mode; + /** + * @bus_flags: the DRM bus flags for this panel e.g. inverted clock + */ + u32 bus_flags; +}; + +/** + * struct tpg110 - state container for the TPG110 panel + */ +struct tpg110 { + /** + * @dev: the container device + */ + struct device *dev; + /** + * @spi: the corresponding SPI device + */ + struct spi_device *spi; + /** + * @panel: the DRM panel instance for this device + */ + struct drm_panel panel; + /** + * @backlight: backlight for this panel + */ + struct backlight_device *backlight; + /** + * @panel_type: the panel mode as detected + */ + const struct tpg110_panel_mode *panel_mode; + /** + * @width: the width of this panel in mm + */ + u32 width; + /** + * @height: the height of this panel in mm + */ + u32 height; + /** + * @grestb: reset GPIO line + */ + struct gpio_desc *grestb; +}; + +/* + * TPG110 modes, these are the simple modes, the dualscan modes that + * take 400x240 or 480x272 in and display as 800x480 are not listed. + */ +static const struct tpg110_panel_mode tpg110_modes[] = { + { + .name = "800x480 RGB", + .magic = TPG110_RES_800X480, + .mode = { + .clock = 33200, + .hdisplay = 800, + .hsync_start = 800 + 40, + .hsync_end = 800 + 40 + 1, + .htotal = 800 + 40 + 1 + 216, + .vdisplay = 480, + .vsync_start = 480 + 10, + .vsync_end = 480 + 10 + 1, + .vtotal = 480 + 10 + 1 + 35, + .vrefresh = 60, + }, + .bus_flags = DRM_BUS_FLAG_PIXDATA_POSEDGE, + }, + { + .name = "640x480 RGB", + .magic = TPG110_RES_640X480, + .mode = { + .clock = 25200, + .hdisplay = 640, + .hsync_start = 640 + 24, + .hsync_end = 640 + 24 + 1, + .htotal = 640 + 24 + 1 + 136, + .vdisplay = 480, + .vsync_start = 480 + 18, + .vsync_end = 480 + 18 + 1, + .vtotal = 480 + 18 + 1 + 27, + .vrefresh = 60, + }, + .bus_flags = DRM_BUS_FLAG_PIXDATA_POSEDGE, + }, + { + .name = "480x272 RGB", + .magic = TPG110_RES_480X272, + .mode = { + .clock = 9000, + .hdisplay = 480, + .hsync_start = 480 + 2, + .hsync_end = 480 + 2 + 1, + .htotal = 480 + 2 + 1 + 43, + .vdisplay = 272, + .vsync_start = 272 + 2, + .vsync_end = 272 + 2 + 1, + .vtotal = 272 + 2 + 1 + 12, + .vrefresh = 60, + }, + .bus_flags = DRM_BUS_FLAG_PIXDATA_POSEDGE, + }, + { + .name = "480x640 RGB", + .magic = TPG110_RES_480X640, + .mode = { + .clock = 20500, + .hdisplay = 480, + .hsync_start = 480 + 2, + .hsync_end = 480 + 2 + 1, + .htotal = 480 + 2 + 1 + 43, + .vdisplay = 640, + .vsync_start = 640 + 4, + .vsync_end = 640 + 4 + 1, + .vtotal = 640 + 4 + 1 + 8, + .vrefresh = 60, + }, + .bus_flags = DRM_BUS_FLAG_PIXDATA_POSEDGE, + }, + { + .name = "400x240 RGB", + .magic = TPG110_RES_400X240_D, + .mode = { + .clock = 8300, + .hdisplay = 400, + .hsync_start = 400 + 20, + .hsync_end = 400 + 20 + 1, + .htotal = 400 + 20 + 1 + 108, + .vdisplay = 240, + .vsync_start = 240 + 2, + .vsync_end = 240 + 2 + 1, + .vtotal = 240 + 2 + 1 + 20, + .vrefresh = 60, + }, + .bus_flags = DRM_BUS_FLAG_PIXDATA_POSEDGE, + }, +}; + +static inline struct tpg110 * +to_tpg110(struct drm_panel *panel) +{ + return container_of(panel, struct tpg110, panel); +} + +static u8 tpg110_readwrite_reg(struct tpg110 *tpg, bool write, + u8 address, u8 outval) +{ + struct spi_message m; + struct spi_transfer t[2]; + u8 buf[2]; + int ret; + + spi_message_init(&m); + memset(t, 0, sizeof(t)); + + if (write) { + /* + * Clear address bit 0, 1 when writing, just to be sure + * The actual bit indicating a write here is bit 1, bit + * 0 is just surplus to pad it up to 8 bits. + */ + buf[0] = address << 2; + buf[0] &= ~0x03; + buf[1] = outval; + + t[0].bits_per_word = 8; + t[0].tx_buf = &buf[0]; + t[0].len = 1; + + t[1].tx_buf = &buf[1]; + t[1].len = 1; + t[1].bits_per_word = 8; + } else { + /* Set address bit 0 to 1 to read */ + buf[0] = address << 1; + buf[0] |= 0x01; + + /* + * The last bit/clock is Hi-Z turnaround cycle, so we need + * to send only 7 bits here. The 8th bit is the high impedance + * turn-around cycle. + */ + t[0].bits_per_word = 7; + t[0].tx_buf = &buf[0]; + t[0].len = 1; + + t[1].rx_buf = &buf[1]; + t[1].len = 1; + t[1].bits_per_word = 8; + } + + spi_message_add_tail(&t[0], &m); + spi_message_add_tail(&t[1], &m); + ret = spi_sync(tpg->spi, &m); + if (ret) { + dev_err(tpg->dev, "SPI message error %d\n", ret); + return ret; + } + if (write) + return 0; + /* Read */ + return buf[1]; +} + +static u8 tpg110_read_reg(struct tpg110 *tpg, u8 address) +{ + return tpg110_readwrite_reg(tpg, false, address, 0); +} + +static void tpg110_write_reg(struct tpg110 *tpg, u8 address, u8 outval) +{ + tpg110_readwrite_reg(tpg, true, address, outval); +} + +static int tpg110_startup(struct tpg110 *tpg) +{ + u8 val; + int i; + + /* De-assert the reset signal */ + gpiod_set_value_cansleep(tpg->grestb, 0); + mdelay(1); + dev_info(tpg->dev, "de-asserted GRESTB\n"); + + /* Test display communication */ + tpg110_write_reg(tpg, TPG110_TEST, 0x55); + val = tpg110_read_reg(tpg, TPG110_TEST); + if (val != 0x55) { + dev_err(tpg->dev, "failed communication test\n"); + return -ENODEV; + } + + val = tpg110_read_reg(tpg, TPG110_CHIPID); + dev_info(tpg->dev, "TPG110 chip ID: %d version: %d\n", + val >> 4, val & 0x0f); + + /* Show display resolution */ + val = tpg110_read_reg(tpg, TPG110_CTRL1); + val &= TPG110_RES_MASK; + switch (val) { + case TPG110_RES_400X240_D: + dev_info(tpg->dev, + "IN 400x240 RGB -> OUT 800x480 RGB (dual scan)"); + break; + case TPG110_RES_480X272_D: + dev_info(tpg->dev, + "IN 480x272 RGB -> OUT 800x480 RGB (dual scan)"); + break; + case TPG110_RES_480X640: + dev_info(tpg->dev, "480x640 RGB"); + break; + case TPG110_RES_480X272: + dev_info(tpg->dev, "480x272 RGB"); + break; + case TPG110_RES_640X480: + dev_info(tpg->dev, "640x480 RGB"); + break; + case TPG110_RES_800X480: + dev_info(tpg->dev, "800x480 RGB"); + break; + default: + dev_info(tpg->dev, "ILLEGAL RESOLUTION"); + break; + } + + /* From the producer side, this is the same resolution */ + if (val == TPG110_RES_480X272_D) + val = TPG110_RES_480X272; + + for (i = 0; i < ARRAY_SIZE(tpg110_modes); i++) { + const struct tpg110_panel_mode *pm; + + pm = &tpg110_modes[i]; + if (pm->magic == val) { + tpg->panel_mode = pm; + break; + } + } + if (i == ARRAY_SIZE(tpg110_modes)) { + dev_err(tpg->dev, "unsupported mode (%02x) detected\n", + val); + return -ENODEV; + } + + val = tpg110_read_reg(tpg, TPG110_CTRL2); + dev_info(tpg->dev, "resolution and standby is controlled by %s\n", + (val & TPG110_CTRL2_RES_PM_CTRL) ? "software" : "hardware"); + /* Take control over resolution and standby */ + val |= TPG110_CTRL2_RES_PM_CTRL; + tpg110_write_reg(tpg, TPG110_CTRL2, val); + + return 0; +} + +static int tpg110_disable(struct drm_panel *panel) +{ + struct tpg110 *tpg = to_tpg110(panel); + u8 val; + + /* Put chip into standby */ + val = tpg110_read_reg(tpg, TPG110_CTRL2_PM); + val &= ~TPG110_CTRL2_PM; + tpg110_write_reg(tpg, TPG110_CTRL2_PM, val); + + if (tpg->backlight) { + tpg->backlight->props.power = FB_BLANK_POWERDOWN; + tpg->backlight->props.state |= BL_CORE_FBBLANK; + backlight_update_status(tpg->backlight); + } + + return 0; +} + +static int tpg110_enable(struct drm_panel *panel) +{ + struct tpg110 *tpg = to_tpg110(panel); + u8 val; + + if (tpg->backlight) { + tpg->backlight->props.state &= ~BL_CORE_FBBLANK; + tpg->backlight->props.power = FB_BLANK_UNBLANK; + backlight_update_status(tpg->backlight); + } + + /* Take chip out of standby */ + val = tpg110_read_reg(tpg, TPG110_CTRL2_PM); + val |= TPG110_CTRL2_PM; + tpg110_write_reg(tpg, TPG110_CTRL2_PM, val); + + return 0; +} + +/** + * tpg110_get_modes() - return the appropriate mode + * @panel: the panel to get the mode for + * + * This currently does not present a forest of modes, instead it + * presents the mode that is configured for the system under use, + * and which is detected by reading the registers of the display. + */ +static int tpg110_get_modes(struct drm_panel *panel) +{ + struct drm_connector *connector = panel->connector; + struct tpg110 *tpg = to_tpg110(panel); + struct drm_display_mode *mode; + + strncpy(connector->display_info.name, tpg->panel_mode->name, + DRM_DISPLAY_INFO_LEN); + connector->display_info.width_mm = tpg->width; + connector->display_info.height_mm = tpg->height; + connector->display_info.bus_flags = tpg->panel_mode->bus_flags; + + mode = drm_mode_duplicate(panel->drm, &tpg->panel_mode->mode); + drm_mode_set_name(mode); + mode->type = DRM_MODE_TYPE_DRIVER | DRM_MODE_TYPE_PREFERRED; + + mode->width_mm = tpg->width; + mode->height_mm = tpg->height; + + drm_mode_probed_add(connector, mode); + + return 1; +} + +static const struct drm_panel_funcs tpg110_drm_funcs = { + .disable = tpg110_disable, + .enable = tpg110_enable, + .get_modes = tpg110_get_modes, +}; + +static int tpg110_probe(struct spi_device *spi) +{ + struct device *dev = &spi->dev; + struct device_node *np = dev->of_node; + struct device_node *backlight; + struct tpg110 *tpg; + int ret; + + tpg = devm_kzalloc(dev, sizeof(*tpg), GFP_KERNEL); + if (!tpg) + return -ENOMEM; + tpg->dev = dev; + + /* We get the physical display dimensions from the DT */ + ret = of_property_read_u32(np, "width-mm", &tpg->width); + if (ret) + dev_err(dev, "no panel width specified\n"); + ret = of_property_read_u32(np, "height-mm", &tpg->height); + if (ret) + dev_err(dev, "no panel height specified\n"); + + /* Look for some optional backlight */ + backlight = of_parse_phandle(np, "backlight", 0); + if (backlight) { + tpg->backlight = of_find_backlight_by_node(backlight); + of_node_put(backlight); + + if (!tpg->backlight) + return -EPROBE_DEFER; + } + + /* This asserts the GRESTB signal, putting the display into reset */ + tpg->grestb = devm_gpiod_get(dev, "grestb", GPIOD_OUT_HIGH); + if (IS_ERR(tpg->grestb)) { + dev_err(dev, "no GRESTB GPIO\n"); + return -ENODEV; + } + + spi->bits_per_word = 8; + spi->mode |= SPI_3WIRE_HIZ; + ret = spi_setup(spi); + if (ret < 0) { + dev_err(dev, "spi setup failed.\n"); + return ret; + } + tpg->spi = spi; + + ret = tpg110_startup(tpg); + if (ret) + return ret; + + drm_panel_init(&tpg->panel); + tpg->panel.dev = dev; + tpg->panel.funcs = &tpg110_drm_funcs; + spi_set_drvdata(spi, tpg); + + return drm_panel_add(&tpg->panel); +} + +static int tpg110_remove(struct spi_device *spi) +{ + struct tpg110 *tpg = spi_get_drvdata(spi); + + drm_panel_remove(&tpg->panel); + return 0; +} + +static const struct of_device_id tpg110_match[] = { + { .compatible = "tpo,tpg110", }, + {}, +}; +MODULE_DEVICE_TABLE(of, tpg110_match); + +static struct spi_driver tpg110_driver = { + .probe = tpg110_probe, + .remove = tpg110_remove, + .driver = { + .name = "tpo-tpg110-panel", + .of_match_table = tpg110_match, + }, +}; +module_spi_driver(tpg110_driver); + +MODULE_AUTHOR("Linus Walleij <linus.walleij@linaro.org>"); +MODULE_DESCRIPTION("TPO TPG110 panel driver"); +MODULE_LICENSE("GPL v2");