diff mbox series

[6/6] drm: tiny: gdepaper: add driver for 2/3 color epaper displays

Message ID 95b64347-fbc8-ba3d-79da-9de2557ff95e@jaseg.net (mailing list archive)
State New, archived
Headers show
Series tiny: Add driver for gooddisplay epaper panels | expand

Commit Message

Jan Sebastian Götte July 30, 2019, 1:48 p.m. UTC
These ePaper displays are made in b/w, b/w/red and b/w/yellow variations
by Good Display (www.e-paper-display.com) and sold on breakout boards by
waveshare (waveshare.com).

This driver was tested against a 2.7" b/w/r display (IL91874 driver) as
well as 4.2" b/w/r display (IL0389 driver) but can probably be
configured for other displays using only a single COG driver die (i.e.
any model except for their largest 12.5" model).

Through tinydrm this driver presents a standard XRGB8888 or RGB565
framebuffer device to userspace and internally converts RGB colors to
the epaper's colors. RGB565 support is provided for compatibility with
anything expecting a small-size TFT LCD whose drivers often support
RGB565.

For userspace, the most notable difference between these epaper displays
and more traditional TFT LCDs besides their limited color space is their
enormously long refresh times-about 5s for a b/w display and >15s for
b/w/r and b/w/y.

This driver uses the display's "4-line SPI" interface (~CS, SCK, MOSI,
BUSY) and does not support the bi-directional "3-line SPI" interface
since target support for bidirectional SPI is going to be patchy. This
means it can't read the display drivers factory-programmed ROM to
automatically recognize the display's model, resolution or color
configuration. This information must be provided from the device tree,
either with a compatible="" section containing the display's model or
through manual configuration of its various parameters. See the device
tree doc file for details.

We name this driver "gdepaper" instead of naming it after the driver
IC's part number since Good Display seems to be using a large number of
mutually compatible distinct variations of the same driver IC, named
using several different numbering schemes. Our plan is to add this
generic driver, and to provide support for individual chips' quirks as
they are discovered.

This driver uses four ioctls:
* Force full display refresh, which may be needed since some epaper
  displays need to be refreshed periodically (every few minutes to
  hours) or the image degrades. This ioctl allows a user program to
  trigger this hardware refresh from the epapers internal frame buffer
  without need for a full redraw.
* Get/set refresh params, which controls the display's waveform LUTs and
  voltage levels.  This is necessary in case a user wants to tune paint/
  refresh performance to their particular content and temperature.  In
  particular for acceptable-quality partial refresh on BWR/BWY panels
  this is essential.
* Enable partial mode, which enables partial update and refresh on fb
  updates. By default this is disabled since careful LUT tuning is
  required for this to not look terrible. Also not all content is suited
  to this mode of operation.

Tested-by: Jan Sebastian Götte <linux@jaseg.net>
Signed-off-by: Jan Sebastian Götte <linux@jaseg.net>
---
 drivers/gpu/drm/tinydrm/gdepaper.c        | 1262 +++++++++++++++++++++
 drivers/gpu/drm/tinydrm/gdepaper_models.h |  157 +++
 2 files changed, 1419 insertions(+)
 create mode 100644 drivers/gpu/drm/tinydrm/gdepaper.c
 create mode 100644 drivers/gpu/drm/tinydrm/gdepaper_models.h

Comments

Noralf Trønnes Aug. 6, 2019, 4:06 p.m. UTC | #1
Hi Jan,

A couple of drive by comments:

Den 30.07.2019 15.48, skrev Jan Sebastian Götte:
> These ePaper displays are made in b/w, b/w/red and b/w/yellow variations
> by Good Display (www.e-paper-display.com) and sold on breakout boards by
> waveshare (waveshare.com).
> 
> This driver was tested against a 2.7" b/w/r display (IL91874 driver) as
> well as 4.2" b/w/r display (IL0389 driver) but can probably be
> configured for other displays using only a single COG driver die (i.e.
> any model except for their largest 12.5" model).
> 
> Through tinydrm this driver presents a standard XRGB8888 or RGB565
> framebuffer device to userspace and internally converts RGB colors to
> the epaper's colors. RGB565 support is provided for compatibility with
> anything expecting a small-size TFT LCD whose drivers often support
> RGB565.
> 
> For userspace, the most notable difference between these epaper displays
> and more traditional TFT LCDs besides their limited color space is their
> enormously long refresh times-about 5s for a b/w display and >15s for
> b/w/r and b/w/y.
> 
> This driver uses the display's "4-line SPI" interface (~CS, SCK, MOSI,
> BUSY) and does not support the bi-directional "3-line SPI" interface
> since target support for bidirectional SPI is going to be patchy. This
> means it can't read the display drivers factory-programmed ROM to
> automatically recognize the display's model, resolution or color
> configuration. This information must be provided from the device tree,
> either with a compatible="" section containing the display's model or
> through manual configuration of its various parameters. See the device
> tree doc file for details.
> 
> We name this driver "gdepaper" instead of naming it after the driver
> IC's part number since Good Display seems to be using a large number of
> mutually compatible distinct variations of the same driver IC, named
> using several different numbering schemes. Our plan is to add this
> generic driver, and to provide support for individual chips' quirks as
> they are discovered.
> 
> This driver uses four ioctls:
> * Force full display refresh, which may be needed since some epaper
>   displays need to be refreshed periodically (every few minutes to
>   hours) or the image degrades. This ioctl allows a user program to
>   trigger this hardware refresh from the epapers internal frame buffer
>   without need for a full redraw.
> * Get/set refresh params, which controls the display's waveform LUTs and
>   voltage levels.  This is necessary in case a user wants to tune paint/
>   refresh performance to their particular content and temperature.  In
>   particular for acceptable-quality partial refresh on BWR/BWY panels
>   this is essential.
> * Enable partial mode, which enables partial update and refresh on fb
>   updates. By default this is disabled since careful LUT tuning is
>   required for this to not look terrible. Also not all content is suited
>   to this mode of operation.
> 
> Tested-by: Jan Sebastian Götte <linux@jaseg.net>

No need for this, it's assumed that you have tested your own driver ;-)

> Signed-off-by: Jan Sebastian Götte <linux@jaseg.net>
> ---
>  drivers/gpu/drm/tinydrm/gdepaper.c        | 1262 +++++++++++++++++++++
>  drivers/gpu/drm/tinydrm/gdepaper_models.h |  157 +++
>  2 files changed, 1419 insertions(+)
>  create mode 100644 drivers/gpu/drm/tinydrm/gdepaper.c
>  create mode 100644 drivers/gpu/drm/tinydrm/gdepaper_models.h
> 
> diff --git a/drivers/gpu/drm/tinydrm/gdepaper.c b/drivers/gpu/drm/tinydrm/gdepaper.c

<snip>

> +static int gdepaper_spi_transfer_cstoggle(struct gdepaper *epap, u8 *data,
> +					  size_t len)
> +{
> +	int i, ret = 0;
> +
> +	for (i = 0; i < len; i++) {
> +		ret = tinydrm_spi_transfer(epap->spi, epap->spi_speed_hz,
> +					   NULL, 8, &data[i], 1);

tinydrm_spi_transfer() is gone now, but you can use spi_write() here. It
will run the transfer at ->max_speed_hz. The reason for
tinydrm_spi_transfer() to have a speed argument, is that some display
controllers can run pixel transfers at a higher speed than the commands,
hence the need to override and run commands at a lower speed.

> +		if (ret)
> +			return ret;
> +		udelay(1); /* FIXME necessary? */
> +	}
> +
> +	return ret;
> +}

<snip>

> +static void gdepaper_fb_dirty(struct drm_framebuffer *fb, struct drm_rect *rect)
> +{
> +	struct gdepaper *epap = drm_to_gdepaper(fb->dev);
> +	struct device *dev = epap->drm.dev;
> +	unsigned int w, h;
> +	struct drm_rect rect_aligned;
> +	int idx, ret = 0;
> +
> +	dev_dbg(dev, "fbdirty\n");
> +
> +	if (!epap->partial_update_en) {
> +		*rect = (struct drm_rect) {
> +			.x1 = 0,
> +			.x2 = fb->width,
> +			.y1 = 0,
> +			.y2 = fb->height,
> +		};
> +	}
> +	w = rect->x2 - rect->x1;
> +	h = rect->y2 - rect->y1;
> +
> +	if (!epap->enabled) {
> +		dev_dbg(dev, "panel is disabled, returning\n");
> +		return;
> +	}
> +
> +	if (!drm_dev_enter(fb->dev, &idx)) {
> +		dev_dbg(dev, "can't acquire drm dev lock\n");
> +		return;
> +	}
> +
> +	dev_dbg(dev, "Flushing [FB:%d] " DRM_RECT_FMT "\n", fb->base.id,
> +		      DRM_RECT_ARG(rect));
> +
> +	ret = gdepaper_power_on(epap);
> +	if (ret)
> +		goto err_out;
> +
> +	if (w == fb->width && h == fb->height) { /* full refresh */
> +		/* black */
> +		ret = gdepaper_txbuf_pack((u8 *)epap->tx_buf, fb, rect,
> +					  GDEP_CH_BLACK);
> +
> +		dev_dbg(dev, "Sending %d byte full framebuf\n", ret);
> +		ret = gdepaper_command(epap, GDEP_CMD_DATA_START_TX_COL1,
> +			(u8 *)epap->tx_buf, ret);
> +		if (ret)
> +			goto err_out;
> +
> +		/* red/yellow */
> +		ret = gdepaper_txbuf_pack((u8 *)epap->tx_buf, fb, rect,
> +					  GDEP_CH_RED_YELLOW);
> +
> +		ret = gdepaper_command(epap, GDEP_CMD_DATA_START_TX_COL2,
> +			(u8 *)epap->tx_buf, ret);
> +		if (ret)
> +			goto err_out;
> +
> +		ret = gdepaper_command(epap, GDEP_CMD_DATA_STOP, NULL, 0);
> +		if (ret)
> +			goto err_out;
> +
> +		ret = gdepaper_command(epap, GDEP_CMD_DISP_RF, NULL, 0);
> +		if (ret)
> +			goto err_out;
> +
> +	} else {
> +		rect_aligned.x1 = rect->x1 & (~7U);

This can also be written as: round_down(rect->x1, 8);

> +		rect_aligned.y1 = rect->y1;
> +		rect_aligned.y2 = rect->y2;
> +		rect_aligned.x2 = rect_aligned.x1 +
> +			  ((rect->x2 - rect_aligned.x1 + 7) & (~7U));

Are you rounding up here?
If so: rect_aligned.x2 = round_up(rect->x2, 8);

> +
> +		/* black */
> +		ret = gdepaper_txbuf_pack((u8 *)epap->tx_buf, fb, &rect_aligned,
> +					  GDEP_CH_BLACK);
> +		dev_dbg(dev, "Sending %d byte partial framebuf\n", ret);
> +		if (ret < 0)
> +			goto err_out;
> +		ret = gdepaper_partial_cmd(epap, &rect_aligned,
> +			GDEP_CMD_PD_START_TX_COL1, (u8 *)epap->tx_buf, ret);
> +		if (ret)
> +			goto err_out;
> +
> +		/* red/yellow */
> +		ret = gdepaper_txbuf_pack((u8 *)epap->tx_buf, fb, &rect_aligned,
> +					  GDEP_CH_RED_YELLOW);
> +		if (ret < 0)
> +			goto err_out;
> +		ret = gdepaper_partial_cmd(epap, &rect_aligned,
> +			GDEP_CMD_PD_START_TX_COL2, (u8 *)epap->tx_buf,
> +			ret);
> +		if (ret)
> +			goto err_out;
> +		ret = gdepaper_command(epap, GDEP_CMD_DATA_STOP, NULL, 0);
> +		if (ret)
> +			goto err_out;
> +		ret = gdepaper_partial_cmd(epap, rect, GDEP_CMD_PART_DISP_RF,
> +			NULL, 0);
> +		if (ret)
> +			goto err_out;
> +	}
> +
> +	if (gdepaper_wait_busy(epap)) {
> +		dev_err(dev, "Timeout on partial refresh cmd\n");
> +		goto err_out;
> +	}
> +
> +	ret = gdepaper_power_off(epap);
> +	if (ret)
> +		goto err_out;
> +
> +	drm_dev_exit(idx);
> +	return;
> +
> +err_out:
> +	/* Try to power off anyway */
> +	gdepaper_power_off(epap);
> +
> +	dev_err(fb->dev->dev, "Failed to update display %d\n", ret);
> +	drm_dev_exit(idx);
> +}

<snip>

> +static int gdepaper_probe(struct spi_device *spi)
> +{
> +	struct device *dev = &spi->dev;
> +	struct device_node *np = dev->of_node;
> +	const struct of_device_id *of_id;
> +	struct drm_device *drm;
> +	struct drm_display_mode *mode;
> +	struct gdepaper *epap;
> +	const struct gdepaper_type_descriptor *type_desc;
> +	int ret;
> +	size_t bufsize;
> +
> +	of_id = of_match_node(gdepaper_of_match, np);
> +	if (WARN_ON(of_id == NULL)) {
> +		dev_warn(dev, "dt node didn't match, aborting probe\n");
> +		return -EINVAL;
> +	}
> +	type_desc = of_id->data;
> +
> +	dev_dbg(dev, "Probing gdepaper module\n");
> +	epap = kzalloc(sizeof(*epap), GFP_KERNEL);
> +	if (!epap)
> +		return -ENOMEM;
> +
> +	epap->enabled = false;
> +	mutex_init(&epap->cmdlock);
> +	epap->tx_buf = NULL;
> +	epap->spi = spi;
> +
> +	drm = &epap->drm;
> +	ret = devm_drm_dev_init(dev, drm, &gdepaper_driver);
> +	if (ret) {
> +		dev_warn(dev, "failed to init drm dev\n");
> +		goto err_free;
> +	}
> +
> +	drm_mode_config_init(drm);
> +
> +	epap->reset = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH);
> +	if (IS_ERR(epap->reset)) {
> +		dev_err(dev, "Failed to get reset GPIO\n");
> +		ret = PTR_ERR(epap->reset);
> +		goto err_free;
> +	}
> +
> +	epap->busy = devm_gpiod_get(dev, "busy", GPIOD_IN);
> +	if (IS_ERR(epap->busy)) {
> +		dev_err(dev, "Failed to get busy GPIO\n");
> +		ret = PTR_ERR(epap->busy);
> +		goto err_free;
> +	}
> +
> +	epap->dc = devm_gpiod_get(dev, "dc", GPIOD_OUT_LOW);
> +	if (IS_ERR(epap->dc)) {
> +		dev_err(dev, "Failed to get dc GPIO\n");
> +		ret = PTR_ERR(epap->dc);
> +		goto err_free;
> +	}
> +
> +	epap->spi_speed_hz = 2000000;
> +	epap->pll_div = 1;
> +	epap->framerate_mHz = 81850;
> +	epap->rfp.vg_lv = GDEP_PWR_VGHL_16V;
> +	epap->rfp.vcom_sel = 0;
> +	epap->rfp.vdh_bw_mv = 11000; /* drive high level, b/w pixel */
> +	epap->rfp.vdh_col_mv = 4200; /* drive high level, red/yellow pixel */
> +	epap->rfp.vdl_mv = -11000; /* drive low level */
> +	epap->rfp.border_data_sel = 2; /* "vbd" */
> +	epap->rfp.data_polarity = 0; /* "ddx" */
> +	epap->rfp.vcom_dc_mv = -1000;
> +	epap->rfp.vcom_data_ivl_hsync = 10; /* hsync periods */
> +	epap->rfp.use_otp_luts_flag = 1;
> +	epap->ss_param[0] = 0x07;
> +	epap->ss_param[1] = 0x07;
> +	epap->ss_param[2] = 0x17;
> +	epap->controller_res = GDEP_CTRL_RES_320X300;
> +
> +	ret = gdepaper_of_read_luts(epap, np, dev);
> +	if (ret) {
> +		dev_warn(dev, "can't read LUTs from dt\n");
> +		goto err_free;
> +	}
> +
> +	of_property_read_u32(np, "controller-resolution",
> +			&epap->controller_res);
> +	of_property_read_u32(np, "spi-speed-hz", &epap->spi_speed_hz);
> +	epap->partial_update_en = of_property_read_bool(np, "partial-update");
> +	ret = of_property_read_u32(np, "colors", &epap->display_colors);
> +	if (ret == -EINVAL) {
> +		if (type_desc) {
> +			epap->display_colors = type_desc->colors;
> +
> +		} else {
> +			dev_err(dev, "colors must be set in dt\n");
> +			ret = -EINVAL;
> +			goto err_free;
> +		}
> +	} else if (ret) {
> +		dev_err(dev, "Invalid dt colors property\n");
> +		goto err_free;
> +	}
> +	if (epap->display_colors < 0 ||
> +			epap->display_colors >= GDEPAPER_COL_END) {
> +		dev_err(dev, "invalid colors value\n");
> +		ret = -EINVAL;
> +		goto err_free;
> +	}
> +	epap->mirror_x = of_property_read_bool(np, "mirror-x");
> +	epap->mirror_y = of_property_read_bool(np, "mirror-y");
> +	of_property_read_u32(np, "pll-div", &epap->pll_div);
> +	of_property_read_u32(np, "fps-millihertz", &epap->framerate_mHz);
> +	of_property_read_u32(np, "vghl-level", &epap->rfp.vg_lv);
> +	epap->vds_en = !of_property_read_bool(np, "vds-external");
> +	epap->vdg_en = !of_property_read_bool(np, "vdg-external");
> +	of_property_read_u32(np, "vcom", &epap->rfp.vcom_sel);
> +	of_property_read_u32(np, "vdh-bw-millivolts", &epap->rfp.vdh_bw_mv);
> +	of_property_read_u32(np, "vdh-color-millivolts", &epap->rfp.vdh_col_mv);
> +	of_property_read_u32(np, "vdl-millivolts", &epap->rfp.vdl_mv);
> +	of_property_read_u32(np, "border-data", &epap->rfp.border_data_sel);
> +	of_property_read_u32(np, "data-polarity", &epap->rfp.data_polarity);
> +	ret = of_property_read_u8_array(np, "boost-soft-start",
> +			(u8 *)&epap->ss_param, sizeof(epap->ss_param));
> +	if (ret && ret != -EINVAL)
> +		dev_err(dev, "invalid boost-soft-start value, ignoring\n");
> +	of_property_read_u32(np, "vcom-data-interval-periods",
> +			&epap->rfp.vcom_data_ivl_hsync);

Why do you need these DT properties when you define compatibles for all
the panels, can't you include these settings in the type descriptor?

> +
> +	/* Accept both positive and negative notation */
> +	if (epap->rfp.vdl_mv < 0)
> +		epap->rfp.vdl_mv = -epap->rfp.vdl_mv;
> +	if (epap->rfp.vcom_dc_mv < 0)
> +		epap->rfp.vcom_dc_mv = -epap->rfp.vcom_dc_mv;
> +
> +	/* (from mipi-dbi.c:)
> +	 * Even though it's not the SPI device that does DMA (the master does),
> +	 * the dma mask is necessary for the dma_alloc_wc() in
> +	 * drm_gem_cma_create(). The dma_addr returned will be a physical
> +	 * address which might be different from the bus address, but this is
> +	 * not a problem since the address will not be used.
> +	 * The virtual address is used in the transfer and the SPI core
> +	 * re-maps it on the SPI master device using the DMA streaming API
> +	 * (spi_map_buf()).
> +	 */
> +	if (!dev->coherent_dma_mask) {
> +		ret = dma_coerce_mask_and_coherent(dev, DMA_BIT_MASK(32));
> +		if (ret) {
> +			dev_warn(dev, "Failed to set dma mask %d\n", ret);
> +			goto err_free;
> +		}
> +	}
> +
> +	mode = gdepaper_of_read_mode(type_desc, np, dev);
> +	if (IS_ERR(mode)) {
> +		dev_warn(dev, "Failed to read mode: %ld\n", PTR_ERR(mode));
> +		ret = PTR_ERR(mode);
> +		goto err_free;
> +	}
> +
> +	/* 8 pixels per byte, bit-packed */
> +	bufsize = (mode->vdisplay * mode->hdisplay + 7)/8;

DIV_ROUND_UP(mode->vdisplay * mode->hdisplay, 8)

> +	epap->tx_buf = devm_kmalloc(drm->dev, bufsize, GFP_KERNEL);
> +	if (!epap->tx_buf) {
> +		ret = -ENOMEM;
> +		goto err_free;
> +	}
> +
> +	/* TODO rotation support? */
> +	ret = tinydrm_display_pipe_init(drm, &epap->pipe, &gdepaper_pipe_funcs,
> +					DRM_MODE_CONNECTOR_VIRTUAL,
> +					gdepaper_formats,
> +					ARRAY_SIZE(gdepaper_formats), mode, 0);

tinydrm_display_pipe_init() is gone now, here's how I replaced it in the
other e-ink driver:

drm/tinydrm/repaper: Don't use tinydrm_display_pipe_init()
https://cgit.freedesktop.org/drm/drm-misc/commit?id=1321db837549a0ff9dc2c95ff76c46770f7f02a1

Noralf.

> +	if (ret) {
> +		dev_warn(dev, "Failed to initialize display pipe: %d\n", ret);
> +		goto err_free;
> +	}
> +
> +	drm->mode_config.funcs = &gdepaper_dbi_mode_config_funcs;
> +	drm->mode_config.preferred_depth = 32;
> +	drm_plane_enable_fb_damage_clips(&epap->pipe.plane);
> +	drm_mode_config_reset(drm);
> +
> +	ret = drm_dev_register(drm, 0);
> +	if (ret) {
> +		dev_warn(dev, "Failed to register drm device: %d\n", ret);
> +		goto err_free;
> +	}
> +
> +	spi_set_drvdata(spi, drm);
> +	drm_fbdev_generic_setup(drm, 0);
> +
> +	dev_dbg(dev, "Probed gdepaper module\n");
> +	return 0;
> +err_free:
> +	kfree(epap);
> +	return ret;
> +}
Jan Sebastian Götte Aug. 10, 2019, 5:17 a.m. UTC | #2
Hi Noralf,

thank you for your comments. I've incorporated your suggestions into my draft.

On 8/7/19 1:06 AM, Noralf Trønnes wrote:
[...]

>> +static int gdepaper_probe(struct spi_device *spi)
>> +{
>> +	struct device *dev = &spi->dev;
>> +	struct device_node *np = dev->of_node;
>> +	const struct of_device_id *of_id;
>> +	struct drm_device *drm;
>> +	struct drm_display_mode *mode;
>> +	struct gdepaper *epap;
>> +	const struct gdepaper_type_descriptor *type_desc;
>> +	int ret;
>> +	size_t bufsize;
>> +
>> +	of_id = of_match_node(gdepaper_of_match, np);
>> +	if (WARN_ON(of_id == NULL)) {
>> +		dev_warn(dev, "dt node didn't match, aborting probe\n");
>> +		return -EINVAL;
>> +	}
>> +	type_desc = of_id->data;
>> +
>> +	dev_dbg(dev, "Probing gdepaper module\n");
>> +	epap = kzalloc(sizeof(*epap), GFP_KERNEL);
>> +	if (!epap)
>> +		return -ENOMEM;
>> +
>> +	epap->enabled = false;
>> +	mutex_init(&epap->cmdlock);
>> +	epap->tx_buf = NULL;
>> +	epap->spi = spi;
>> +
>> +	drm = &epap->drm;
>> +	ret = devm_drm_dev_init(dev, drm, &gdepaper_driver);
>> +	if (ret) {
>> +		dev_warn(dev, "failed to init drm dev\n");
>> +		goto err_free;
>> +	}
>> +
>> +	drm_mode_config_init(drm);
>> +
>> +	epap->reset = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH);
>> +	if (IS_ERR(epap->reset)) {
>> +		dev_err(dev, "Failed to get reset GPIO\n");
>> +		ret = PTR_ERR(epap->reset);
>> +		goto err_free;
>> +	}
>> +
>> +	epap->busy = devm_gpiod_get(dev, "busy", GPIOD_IN);
>> +	if (IS_ERR(epap->busy)) {
>> +		dev_err(dev, "Failed to get busy GPIO\n");
>> +		ret = PTR_ERR(epap->busy);
>> +		goto err_free;
>> +	}
>> +
>> +	epap->dc = devm_gpiod_get(dev, "dc", GPIOD_OUT_LOW);
>> +	if (IS_ERR(epap->dc)) {
>> +		dev_err(dev, "Failed to get dc GPIO\n");
>> +		ret = PTR_ERR(epap->dc);
>> +		goto err_free;
>> +	}
>> +
>> +	epap->spi_speed_hz = 2000000;
>> +	epap->pll_div = 1;
>> +	epap->framerate_mHz = 81850;
>> +	epap->rfp.vg_lv = GDEP_PWR_VGHL_16V;
>> +	epap->rfp.vcom_sel = 0;
>> +	epap->rfp.vdh_bw_mv = 11000; /* drive high level, b/w pixel */
>> +	epap->rfp.vdh_col_mv = 4200; /* drive high level, red/yellow pixel */
>> +	epap->rfp.vdl_mv = -11000; /* drive low level */
>> +	epap->rfp.border_data_sel = 2; /* "vbd" */
>> +	epap->rfp.data_polarity = 0; /* "ddx" */
>> +	epap->rfp.vcom_dc_mv = -1000;
>> +	epap->rfp.vcom_data_ivl_hsync = 10; /* hsync periods */
>> +	epap->rfp.use_otp_luts_flag = 1;
>> +	epap->ss_param[0] = 0x07;
>> +	epap->ss_param[1] = 0x07;
>> +	epap->ss_param[2] = 0x17;
>> +	epap->controller_res = GDEP_CTRL_RES_320X300;
>> +
>> +	ret = gdepaper_of_read_luts(epap, np, dev);
>> +	if (ret) {
>> +		dev_warn(dev, "can't read LUTs from dt\n");
>> +		goto err_free;
>> +	}
>> +
>> +	of_property_read_u32(np, "controller-resolution",
>> +			&epap->controller_res);
>> +	of_property_read_u32(np, "spi-speed-hz", &epap->spi_speed_hz);
>> +	epap->partial_update_en = of_property_read_bool(np, "partial-update");
>> +	ret = of_property_read_u32(np, "colors", &epap->display_colors);
>> +	if (ret == -EINVAL) {
>> +		if (type_desc) {
>> +			epap->display_colors = type_desc->colors;
>> +
>> +		} else {
>> +			dev_err(dev, "colors must be set in dt\n");
>> +			ret = -EINVAL;
>> +			goto err_free;
>> +		}
>> +	} else if (ret) {
>> +		dev_err(dev, "Invalid dt colors property\n");
>> +		goto err_free;
>> +	}
>> +	if (epap->display_colors < 0 ||
>> +			epap->display_colors >= GDEPAPER_COL_END) {
>> +		dev_err(dev, "invalid colors value\n");
>> +		ret = -EINVAL;
>> +		goto err_free;
>> +	}
>> +	epap->mirror_x = of_property_read_bool(np, "mirror-x");
>> +	epap->mirror_y = of_property_read_bool(np, "mirror-y");
>> +	of_property_read_u32(np, "pll-div", &epap->pll_div);
>> +	of_property_read_u32(np, "fps-millihertz", &epap->framerate_mHz);
>> +	of_property_read_u32(np, "vghl-level", &epap->rfp.vg_lv);
>> +	epap->vds_en = !of_property_read_bool(np, "vds-external");
>> +	epap->vdg_en = !of_property_read_bool(np, "vdg-external");
>> +	of_property_read_u32(np, "vcom", &epap->rfp.vcom_sel);
>> +	of_property_read_u32(np, "vdh-bw-millivolts", &epap->rfp.vdh_bw_mv);
>> +	of_property_read_u32(np, "vdh-color-millivolts", &epap->rfp.vdh_col_mv);
>> +	of_property_read_u32(np, "vdl-millivolts", &epap->rfp.vdl_mv);
>> +	of_property_read_u32(np, "border-data", &epap->rfp.border_data_sel);
>> +	of_property_read_u32(np, "data-polarity", &epap->rfp.data_polarity);
>> +	ret = of_property_read_u8_array(np, "boost-soft-start",
>> +			(u8 *)&epap->ss_param, sizeof(epap->ss_param));
>> +	if (ret && ret != -EINVAL)
>> +		dev_err(dev, "invalid boost-soft-start value, ignoring\n");
>> +	of_property_read_u32(np, "vcom-data-interval-periods",
>> +			&epap->rfp.vcom_data_ivl_hsync);
> 
> Why do you need these DT properties when you define compatibles for all
> the panels, can't you include these settings in the type descriptor?
	I allowed for these to be overridden in case there is some panel that's not listed on the mfg's (quite chaotic) website. Looking at this some more I think I'll remove some of these though.

I'll leave vds-external/vgs-external since they depend on circuitry around the panel and thus should be in DT. boost-soft-start is largely undocumented and I don't know what they might be useful for, but I feel it could depend on the booster inductors and voltage regulator connected to the panel, so it should be in DT.

Those ending up in the refresh params struct are refresh-related and thus application-specific. Most of these come with probably sane defaults, so to initialize a display at a minimum you only need either the type (compatible=gdew...) or the dimensions (px, mm) and color scheme.

>> +
>> +	/* Accept both positive and negative notation */
>> +	if (epap->rfp.vdl_mv < 0)
>> +		epap->rfp.vdl_mv = -epap->rfp.vdl_mv;
>> +	if (epap->rfp.vcom_dc_mv < 0)
>> +		epap->rfp.vcom_dc_mv = -epap->rfp.vcom_dc_mv;
>> +
>> +	/* (from mipi-dbi.c:)
>> +	 * Even though it's not the SPI device that does DMA (the master does),
>> +	 * the dma mask is necessary for the dma_alloc_wc() in
>> +	 * drm_gem_cma_create(). The dma_addr returned will be a physical
>> +	 * address which might be different from the bus address, but this is
>> +	 * not a problem since the address will not be used.
>> +	 * The virtual address is used in the transfer and the SPI core
>> +	 * re-maps it on the SPI master device using the DMA streaming API
>> +	 * (spi_map_buf()).
>> +	 */
>> +	if (!dev->coherent_dma_mask) {
>> +		ret = dma_coerce_mask_and_coherent(dev, DMA_BIT_MASK(32));
>> +		if (ret) {
>> +			dev_warn(dev, "Failed to set dma mask %d\n", ret);
>> +			goto err_free;
>> +		}
>> +	}
>> +
>> +	mode = gdepaper_of_read_mode(type_desc, np, dev);
>> +	if (IS_ERR(mode)) {
>> +		dev_warn(dev, "Failed to read mode: %ld\n", PTR_ERR(mode));
>> +		ret = PTR_ERR(mode);
>> +		goto err_free;
>> +	}
>> +
>> +	/* 8 pixels per byte, bit-packed */
>> +	bufsize = (mode->vdisplay * mode->hdisplay + 7)/8;
> 
> DIV_ROUND_UP(mode->vdisplay * mode->hdisplay, 8)
> 
>> +	epap->tx_buf = devm_kmalloc(drm->dev, bufsize, GFP_KERNEL);
>> +	if (!epap->tx_buf) {
>> +		ret = -ENOMEM;
>> +		goto err_free;
>> +	}
>> +
>> +	/* TODO rotation support? */
>> +	ret = tinydrm_display_pipe_init(drm, &epap->pipe, &gdepaper_pipe_funcs,
>> +					DRM_MODE_CONNECTOR_VIRTUAL,
>> +					gdepaper_formats,
>> +					ARRAY_SIZE(gdepaper_formats), mode, 0);
> 
> tinydrm_display_pipe_init() is gone now, here's how I replaced it in the
> other e-ink driver:
> 
> drm/tinydrm/repaper: Don't use tinydrm_display_pipe_init()
> https://cgit.freedesktop.org/drm/drm-misc/commit?id=1321db837549a0ff9dc2c95ff76c46770f7f02a1
Thank you. I found an almost identical solution.

- Jan
diff mbox series

Patch

diff --git a/drivers/gpu/drm/tinydrm/gdepaper.c b/drivers/gpu/drm/tinydrm/gdepaper.c
new file mode 100644
index 000000000000..68ac1282bbde
--- /dev/null
+++ b/drivers/gpu/drm/tinydrm/gdepaper.c
@@ -0,0 +1,1262 @@ 
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * DRM driver for Good Display black/white and black/white/red epaper panels
+ *
+ * Copyright 2019 Jan Sebastian Goette
+ *
+ * Some code copied from ili9225.c
+ * Copyright 2017 David Lechner
+ * Copyright 2016 Noralf Trønnes
+ */
+
+#include <linux/delay.h>
+#include <linux/mutex.h>
+#include <linux/device.h>
+#include <linux/dma-buf.h>
+#include <linux/gpio/consumer.h>
+#include <linux/module.h>
+#include <linux/of_device.h>
+#include <linux/sched/clock.h>
+#include <linux/spi/spi.h>
+#include <linux/thermal.h>
+#include <linux/delay.h>
+
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_damage_helper.h>
+#include <drm/drm_drv.h>
+#include <drm/drm_fb_cma_helper.h>
+#include <drm/drm_fb_helper.h>
+#include <drm/drm_format_helper.h>
+#include <drm/drm_fourcc.h>
+#include <drm/drm_gem_cma_helper.h>
+#include <drm/drm_gem_framebuffer_helper.h>
+#include <drm/drm_rect.h>
+#include <drm/drm_vblank.h>
+#include <drm/drm_simple_kms_helper.h>
+#include <drm/tinydrm/tinydrm-helpers.h>
+
+#include <uapi/drm/gdepaper_drm.h>
+
+#include <dt-bindings/display/gdepaper.h>
+
+
+enum gdepaper_cmd {
+	GDEP_CMD_PANEL_SETUP = 0x00,
+	GDEP_CMD_PWR_SET = 0x01,
+	GDEP_CMD_PWR_OFF = 0x02,
+	GDEP_CMD_PWR_SEQ_SET = 0x03,
+	GDEP_CMD_PWR_ON = 0x04,
+	GDEP_CMD_PWR_MEAS = 0x05,
+	GDEP_CMD_BST_SOFT_START = 0x06,
+	GDEP_CMD_DEEP_SLEEP = 0x07,
+	GDEP_CMD_DATA_START_TX_COL1 = 0x10,
+	GDEP_CMD_DATA_STOP = 0x11,
+	GDEP_CMD_DISP_RF = 0x12,
+	GDEP_CMD_DATA_START_TX_COL2 = 0x13,
+	GDEP_CMD_PD_START_TX_COL1 = 0x14,
+	GDEP_CMD_PD_START_TX_COL2 = 0x15,
+	GDEP_CMD_PART_DISP_RF = 0x16,
+	GDEP_CMD_LUT_VCOM_DC = 0x20,
+	GDEP_CMD_LUT_WW = 0x21,
+	GDEP_CMD_LUT_BW = 0x22,
+	GDEP_CMD_LUT_WB = 0x23,
+	GDEP_CMD_LUT_BB = 0x24,
+	GDEP_CMD_PLL_CTRL = 0x30,
+	GDEP_CMD_TEMP_SENS_CMD = 0x40,
+	GDEP_CMD_TEMP_CAL = 0x41,
+	GDEP_CMD_TEMP_SENS_WR = 0x42,
+	GDEP_CMD_TEMP_SENS_RD = 0x43,
+	GDEP_CMD_VCOM_DIVL_SET = 0x50,
+	GDEP_CMD_LOW_PWR_DET = 0x51,
+	GDEP_CMD_TCON_SET = 0x60,
+	GDEP_CMD_TCON_RES = 0x61,
+	GDEP_CMD_SRC_GATE_SET = 0x62,
+	GDEP_CMD_GET_STATUS = 0x71,
+	GDEP_CMD_VCOM_AUTO_MEAS = 0x80,
+	GDEP_CMD_VCOM_VAL = 0x81,
+	GDEP_CMD_VDC_SET = 0x82,
+	GDEP_CMD_PROG_MODE = 0xa0,
+	GDEP_CMD_ACT_PROG  = 0xa1,
+	GDEP_CMD_READ_OTP = 0xa2,
+	GDEP_CMD_MAGIC1 = 0xf8,
+};
+
+enum gdepaper_psr {
+	GDEP_PSR_OTP_LUT = 0<<5,
+	GDEP_PSR_REG_LUT = 1<<5,
+	GDEP_PSR_COLOR_BWR = 0<<4,
+	GDEP_PSR_COLOR_BW = 1<<4,
+	GDEP_PSR_SCAN_DOWN = 0<<3,
+	GDEP_PSR_SCAN_UP = 1<<3,
+	GDEP_PSR_SH_LEFT = 0<<2,
+	GDEP_PSR_SH_RIGHT = 1<<2,
+	GDEP_PSR_BOOST_OFF = 0<<1,
+	GDEP_PSR_BOOST_ON = 1<<1,
+	GDEP_PSR_SOFT_RST = 1<<0,
+};
+
+enum gdepaper_col_ch {
+	GDEP_CH_BLACK = 0x0,
+	GDEP_CH_RED_YELLOW = 0x4,
+};
+
+
+struct gdepaper {
+	struct drm_device drm;
+	struct drm_simple_display_pipe pipe;
+	struct spi_device *spi;
+
+	struct gpio_desc *reset;
+	struct gpio_desc *dc;
+	struct gpio_desc *busy;
+
+	u8 *tx_buf; /* FIXME initialize this */
+	bool enabled;
+	struct mutex cmdlock;
+	u8 psr; /* Panel setup byte */
+	u32 spi_speed_hz;
+	bool partial_update_en;
+	enum gdepaper_color_type display_colors;
+	bool mirror_x, mirror_y;
+	u32 pll_div; /* 1, 2, 4, 8 */
+	u32 framerate_mHz; /* 20220 - 197610 */
+	enum gdepaper_controller_res controller_res;
+	bool vds_en; /* Internal source voltage enable */
+	bool vdg_en;  /* Internal gate voltage enable */
+	u8 ss_param[3]; /* boost converter soft start parameter */
+	bool is_powered_on;
+	struct gdepaper_refresh_params rfp;
+};
+
+struct gdepaper_type_descriptor {
+	enum gdepaper_color_type colors;
+	int w_mm, h_mm;
+	int w_px, h_px;
+};
+
+
+
+static inline struct gdepaper *drm_to_gdepaper(struct drm_device *drm)
+{
+	return container_of(drm, struct gdepaper, drm);
+}
+
+static int gdepaper_spi_transfer_cstoggle(struct gdepaper *epap, u8 *data,
+					  size_t len)
+{
+	int i, ret = 0;
+
+	for (i = 0; i < len; i++) {
+		ret = tinydrm_spi_transfer(epap->spi, epap->spi_speed_hz,
+					   NULL, 8, &data[i], 1);
+		if (ret)
+			return ret;
+		udelay(1); /* FIXME necessary? */
+	}
+
+	return ret;
+}
+
+static int gdepaper_command(struct gdepaper *epap, u8 cmd,
+				   u8 *par, size_t num)
+{
+	int ret;
+	u8 cmd_buf = cmd;
+
+	dev_dbg(epap->drm.dev, "tx: cmd=0x%x len=%zu buf=%*ph\n",
+		cmd, num, (int)num, par);
+
+	gpiod_set_value_cansleep(epap->dc, 0);
+	ret = tinydrm_spi_transfer(epap->spi, epap->spi_speed_hz, NULL, 8,
+				   &cmd_buf, 1);
+	if (ret || !num)
+		return ret;
+
+	gpiod_set_value_cansleep(epap->dc, 1);
+	udelay(10); /* FIXME needed? */
+
+	ret = gdepaper_spi_transfer_cstoggle(epap, par, num);
+	udelay(10); /* FIXME needed? */
+
+	return ret;
+}
+
+static int gdepaper_wait_busy(struct gdepaper *epap)
+{
+	int i = 18000;
+
+	dev_dbg(epap->drm.dev, "waiting for busy line\n");
+	while (i--) {
+		if (!gpiod_get_value_cansleep(epap->busy))
+			return 0;
+
+		usleep_range(1000, 10000);
+	}
+	return -EBUSY;
+}
+
+static int gdepaper_update_luts(struct gdepaper *epap)
+{
+	int ret;
+
+	dev_dbg(epap->drm.dev, "updating LUTs\n");
+
+	ret = gdepaper_command(epap, GDEP_CMD_LUT_VCOM_DC,
+			       epap->rfp.lut_vcom_dc,
+			       sizeof(epap->rfp.lut_vcom_dc));
+	if (ret)
+		return ret;
+	ret = gdepaper_command(epap, GDEP_CMD_LUT_WW,
+			       epap->rfp.lut_ww,
+			       sizeof(epap->rfp.lut_ww));
+	if (ret)
+		return ret;
+	ret = gdepaper_command(epap, GDEP_CMD_LUT_BW,
+			       epap->rfp.lut_bw,
+			       sizeof(epap->rfp.lut_bw));
+	if (ret)
+		return ret;
+	ret = gdepaper_command(epap, GDEP_CMD_LUT_WB,
+			       epap->rfp.lut_wb,
+			       sizeof(epap->rfp.lut_wb));
+	if (ret)
+		return ret;
+	ret = gdepaper_command(epap, GDEP_CMD_LUT_BB,
+			       epap->rfp.lut_bb,
+			       sizeof(epap->rfp.lut_bb));
+	if (ret)
+		return ret;
+
+	return 0;
+};
+
+/* Power off the boost regulators. This must be done as soon as the display is
+ * updated to avoid burn-in damage if powered on over a long time.
+ */
+static int gdepaper_power_off(struct gdepaper *epap)
+{
+	epap->is_powered_on = false;
+	dev_dbg(epap->drm.dev, "display power off\n");
+	return gdepaper_command(epap, GDEP_CMD_PWR_OFF, NULL, 0);
+}
+
+/* Enter deep sleep mode. Deep sleep mode can only be exited with a reset. */
+static int gdepaper_enter_deep_sleep(struct gdepaper *epap)
+{
+	u8 param = 0xa5;
+
+	epap->is_powered_on = false;
+	dev_dbg(epap->drm.dev, "display deep sleep\n");
+	return gdepaper_command(epap, GDEP_CMD_DEEP_SLEEP,
+				    &param, sizeof(param));
+}
+
+static int gdepaper_power_on(struct gdepaper *epap)
+{
+	struct device *dev = epap->drm.dev;
+	int ret = gdepaper_command(epap, GDEP_CMD_PWR_ON, NULL, 0);
+
+	if (ret)
+		return ret;
+
+	ret = gdepaper_wait_busy(epap);
+	if (ret)
+		dev_err(dev, "Timeout on power on cmd\n");
+
+	epap->is_powered_on = true;
+	return ret;
+}
+
+static void gdepaper_reset(struct gdepaper *epap)
+{
+	gpiod_set_value_cansleep(epap->reset, 0);
+	usleep_range(10000, 20000);
+	gpiod_set_value_cansleep(epap->reset, 1);
+	msleep(200);
+}
+
+/* len must be divisible by 8 */
+static void gdepaper_line_rgb565_to_1bpp(u16 *src, u8 *dst, size_t len,
+					    enum gdepaper_col_ch col)
+{
+	size_t i;
+	int j, bits;
+	u8 out;
+
+	for (i = 0; i < len; i += 8) {
+		out = 0;
+		for (j = 0; j < 8; j++) {
+			bits = (src[i+j] >> (15-2))
+			     | (src[i+j] >> (10-1))
+			     | (src[i+j] >> (4-0));
+			out |= (bits == col) << (7-j);
+		}
+		dst[i/8] = out;
+	}
+}
+
+static void gdepaper_line_xrgb8888_to_1bpp(u32 *src, u8 *dst, size_t len,
+					    enum gdepaper_col_ch col)
+{
+	/* TODO what is the endianness of this buffer? */
+	size_t i;
+	int j, bits;
+	u8 out;
+
+	for (i = 0; i < len; i += 8) {
+		out = 0;
+		for (j = 0; j < 8; j++) {
+			bits = !!(src[i+j] & (1<<23)) << 2
+			     | !!(src[i+j] & (1<<15)) << 1
+			     | !!(src[i+j] & (1<<7)) << 0;
+			out |= (bits == col) << (7-j);
+		}
+		dst[i/8] = out;
+	}
+}
+
+/* Pack a framebuffer into 1bpp msb-first format. clip must be 8-bit aligned in
+ * x1 and x2.
+ */
+static int gdepaper_txbuf_pack(u8 *dst,
+				struct drm_framebuffer *fb,
+				struct drm_rect *clip,
+				enum gdepaper_col_ch col)
+{
+	struct drm_gem_cma_object *cma_obj = drm_fb_cma_get_gem_obj(fb, 0);
+	struct dma_buf_attachment *import_attach = cma_obj->base.import_attach;
+	struct drm_format_name_buf format_name;
+	int ret = 0;
+	void *vaddr = cma_obj->vaddr;
+	size_t len = (clip->x2 - clip->x1);
+	unsigned int y, lines = clip->y2 - clip->y1;
+
+	if (import_attach) {
+		ret = dma_buf_begin_cpu_access(import_attach->dmabuf,
+					       DMA_FROM_DEVICE);
+		if (ret)
+			return ret;
+	}
+
+	vaddr += clip->y1 * fb->pitches[0] +
+		clip->x1 * drm_format_plane_cpp(fb->format->format, 0);
+
+	switch (fb->format->format) {
+	case DRM_FORMAT_RGB565:
+		for (y = 0; y < lines; y++) {
+			gdepaper_line_rgb565_to_1bpp(vaddr, dst, len, col);
+			vaddr += fb->pitches[0];
+			dst += len/8;
+		}
+		break;
+
+	case DRM_FORMAT_XRGB8888:
+		for (y = 0; y < lines; y++) {
+			gdepaper_line_xrgb8888_to_1bpp(vaddr, dst, len, col);
+			vaddr += fb->pitches[0];
+			dst += len/8;
+		}
+		break;
+
+	default:
+		dev_err_once(fb->dev->dev, "Format is not supported: %s\n",
+			     drm_get_format_name(fb->format->format,
+						 &format_name));
+		return -EINVAL;
+	}
+
+	if (import_attach) {
+		ret = dma_buf_end_cpu_access(import_attach->dmabuf,
+					     DMA_FROM_DEVICE);
+		if (ret)
+			return ret;
+	}
+
+	return len*lines/8;
+}
+
+static int gdepaper_partial_cmd(struct gdepaper *epap, struct drm_rect *rect,
+				u8 cmd, u8 *buf, size_t len)
+{
+	int ret;
+	struct {
+		u16 x, y, w, l;
+	} __packed param = {
+		.x = rect->x1,
+		.w = rect->x2 - rect->x1,
+		.y = rect->y1,
+		.l = rect->y2 - rect->y1
+	};
+	dev_dbg(epap->drm.dev, "Running partial command 0x%x on rect %d(0x%x),%d(0x%x) +%d(0x%x),%d(0x%x)\n",
+			       cmd, param.x, param.x, param.y, param.y,
+			       param.w, param.w, param.l, param.l);
+
+	param.x = cpu_to_be16(param.x);
+	param.w = cpu_to_be16(param.w);
+	param.y = cpu_to_be16(param.y);
+	param.l = cpu_to_be16(param.l);
+
+	if (rect->x1 & 7 || rect->x2 & 7)
+		return -EINVAL;
+
+	ret = gdepaper_command(epap, cmd, (u8 *)&param, sizeof(param));
+	if (ret)
+		return ret;
+
+	dev_dbg(epap->drm.dev, "partial payload: len=%zu buf=%*ph\n",
+		len, (int)len, buf);
+	return gdepaper_spi_transfer_cstoggle(epap, buf, len);
+}
+
+int gdepaper_config_refresh(struct gdepaper *epap)
+{
+	struct device *dev = epap->drm.dev;
+	int ret = 0;
+	u8 param;
+	struct {
+		u8 vdg_en:1, vds_en:1, _pad1:6;
+		u8 vg_lv:2, vcom_hv:1, _pad2:5;
+		u8 vdh;
+		u8 vdl;
+		u8 vdhr;
+	} __packed gdep_cmd_pwr_set_param = {
+		.vds_en = epap->vds_en, .vdg_en = epap->vdg_en,
+		.vcom_hv = !!epap->rfp.vcom_sel, .vg_lv = epap->rfp.vg_lv,
+		.vdh = (epap->rfp.vdh_bw_mv-2400)/200,
+		.vdl = (epap->rfp.vdl_mv-2400)/200,
+		.vdhr = (epap->rfp.vdh_col_mv-2400)/200,
+	};
+
+	/* Re-configure PSR to set OTP LUT flag */
+	if (epap->rfp.use_otp_luts_flag)
+		epap->psr &= ~GDEP_PSR_REG_LUT;
+	else
+		epap->psr |= GDEP_PSR_REG_LUT;
+	ret = gdepaper_command(epap, GDEP_CMD_PANEL_SETUP,
+				   &epap->psr, sizeof(epap->psr));
+
+	/* set voltage levels */
+	if (epap->rfp.vg_lv < 0 || epap->rfp.vg_lv > 3) {
+		dev_err_once(dev, "Invalid vg_lv value %d\n", epap->rfp.vg_lv);
+		goto err_out;
+	}
+	if (epap->rfp.vdh_bw_mv < 2400 || epap->rfp.vdh_bw_mv > 11000) {
+		dev_err_once(dev, "vdh_bw=%d out of range (2.4V - 11.0V)\n",
+			     epap->rfp.vdh_bw_mv);
+		goto err_out;
+	}
+	if (epap->rfp.vdl_mv < 2400 || epap->rfp.vdl_mv > 11000) {
+		dev_err_once(dev, "vdl_mv=%d out of range (-11.0V - -2.4V)\n",
+			     epap->rfp.vdl_mv);
+		goto err_out;
+	}
+	if (epap->rfp.vdh_col_mv < 2400 || epap->rfp.vdh_col_mv > 11000) {
+		dev_err_once(dev, "vdh_col_mv=%d out of range (2.4V - 11.0V)\n",
+			     epap->rfp.vdh_col_mv);
+		goto err_out;
+	}
+	ret = gdepaper_command(epap, GDEP_CMD_PWR_SET,
+				   (u8 *)&gdep_cmd_pwr_set_param,
+				   sizeof(gdep_cmd_pwr_set_param));
+	if (ret)
+		goto err_out;
+
+	/* VCOM DC setup */
+	param = (epap->rfp.vcom_dc_mv - 100) / 50;
+	ret = gdepaper_command(epap, GDEP_CMD_VDC_SET, &param, sizeof(param));
+	if (ret)
+		goto err_out;
+
+	/* VCOM and data interval setup */
+	if (epap->rfp.vcom_data_ivl_hsync < 2 ||
+	    epap->rfp.vcom_data_ivl_hsync > 17) {
+		dev_err_once(dev, "Invalid vcom/data ivl setting %d (should be 2-17)\n",
+				  epap->rfp.vcom_data_ivl_hsync);
+		goto err_out;
+	}
+	if (epap->rfp.border_data_sel < 0 || epap->rfp.border_data_sel > 3) {
+		dev_err_once(dev, "Invalid border_data_sel (vbd) setting %d\n",
+				  epap->rfp.border_data_sel);
+		goto err_out;
+	}
+	if (epap->rfp.data_polarity < 0 || epap->rfp.data_polarity > 3) {
+		dev_err_once(dev, "Invalid data polarity (ddx) setting %d\n",
+				  epap->rfp.border_data_sel);
+		goto err_out;
+	}
+	param = epap->rfp.border_data_sel<<6 | epap->rfp.data_polarity<<4;
+	param |= epap->rfp.vcom_data_ivl_hsync;
+	ret = gdepaper_command(epap, GDEP_CMD_VCOM_DIVL_SET,
+			&param, sizeof(param));
+	if (ret)
+		goto err_out;
+
+	/* Upload RAM LUTs */
+	if (!epap->rfp.use_otp_luts_flag)
+		ret = gdepaper_update_luts(epap);
+err_out:
+	return ret;
+}
+
+static void gdepaper_fb_dirty(struct drm_framebuffer *fb, struct drm_rect *rect)
+{
+	struct gdepaper *epap = drm_to_gdepaper(fb->dev);
+	struct device *dev = epap->drm.dev;
+	unsigned int w, h;
+	struct drm_rect rect_aligned;
+	int idx, ret = 0;
+
+	dev_dbg(dev, "fbdirty\n");
+
+	if (!epap->partial_update_en) {
+		*rect = (struct drm_rect) {
+			.x1 = 0,
+			.x2 = fb->width,
+			.y1 = 0,
+			.y2 = fb->height,
+		};
+	}
+	w = rect->x2 - rect->x1;
+	h = rect->y2 - rect->y1;
+
+	if (!epap->enabled) {
+		dev_dbg(dev, "panel is disabled, returning\n");
+		return;
+	}
+
+	if (!drm_dev_enter(fb->dev, &idx)) {
+		dev_dbg(dev, "can't acquire drm dev lock\n");
+		return;
+	}
+
+	dev_dbg(dev, "Flushing [FB:%d] " DRM_RECT_FMT "\n", fb->base.id,
+		      DRM_RECT_ARG(rect));
+
+	ret = gdepaper_power_on(epap);
+	if (ret)
+		goto err_out;
+
+	if (w == fb->width && h == fb->height) { /* full refresh */
+		/* black */
+		ret = gdepaper_txbuf_pack((u8 *)epap->tx_buf, fb, rect,
+					  GDEP_CH_BLACK);
+
+		dev_dbg(dev, "Sending %d byte full framebuf\n", ret);
+		ret = gdepaper_command(epap, GDEP_CMD_DATA_START_TX_COL1,
+			(u8 *)epap->tx_buf, ret);
+		if (ret)
+			goto err_out;
+
+		/* red/yellow */
+		ret = gdepaper_txbuf_pack((u8 *)epap->tx_buf, fb, rect,
+					  GDEP_CH_RED_YELLOW);
+
+		ret = gdepaper_command(epap, GDEP_CMD_DATA_START_TX_COL2,
+			(u8 *)epap->tx_buf, ret);
+		if (ret)
+			goto err_out;
+
+		ret = gdepaper_command(epap, GDEP_CMD_DATA_STOP, NULL, 0);
+		if (ret)
+			goto err_out;
+
+		ret = gdepaper_command(epap, GDEP_CMD_DISP_RF, NULL, 0);
+		if (ret)
+			goto err_out;
+
+	} else {
+		rect_aligned.x1 = rect->x1 & (~7U);
+		rect_aligned.y1 = rect->y1;
+		rect_aligned.y2 = rect->y2;
+		rect_aligned.x2 = rect_aligned.x1 +
+			  ((rect->x2 - rect_aligned.x1 + 7) & (~7U));
+
+		/* black */
+		ret = gdepaper_txbuf_pack((u8 *)epap->tx_buf, fb, &rect_aligned,
+					  GDEP_CH_BLACK);
+		dev_dbg(dev, "Sending %d byte partial framebuf\n", ret);
+		if (ret < 0)
+			goto err_out;
+		ret = gdepaper_partial_cmd(epap, &rect_aligned,
+			GDEP_CMD_PD_START_TX_COL1, (u8 *)epap->tx_buf, ret);
+		if (ret)
+			goto err_out;
+
+		/* red/yellow */
+		ret = gdepaper_txbuf_pack((u8 *)epap->tx_buf, fb, &rect_aligned,
+					  GDEP_CH_RED_YELLOW);
+		if (ret < 0)
+			goto err_out;
+		ret = gdepaper_partial_cmd(epap, &rect_aligned,
+			GDEP_CMD_PD_START_TX_COL2, (u8 *)epap->tx_buf,
+			ret);
+		if (ret)
+			goto err_out;
+		ret = gdepaper_command(epap, GDEP_CMD_DATA_STOP, NULL, 0);
+		if (ret)
+			goto err_out;
+		ret = gdepaper_partial_cmd(epap, rect, GDEP_CMD_PART_DISP_RF,
+			NULL, 0);
+		if (ret)
+			goto err_out;
+	}
+
+	if (gdepaper_wait_busy(epap)) {
+		dev_err(dev, "Timeout on partial refresh cmd\n");
+		goto err_out;
+	}
+
+	ret = gdepaper_power_off(epap);
+	if (ret)
+		goto err_out;
+
+	drm_dev_exit(idx);
+	return;
+
+err_out:
+	/* Try to power off anyway */
+	gdepaper_power_off(epap);
+
+	dev_err(fb->dev->dev, "Failed to update display %d\n", ret);
+	drm_dev_exit(idx);
+}
+
+static void gdepaper_pipe_enable(struct drm_simple_display_pipe *pipe,
+				 struct drm_crtc_state *crtc_state,
+				 struct drm_plane_state *plane_state)
+{
+	struct gdepaper *epap = drm_to_gdepaper(pipe->crtc.dev);
+	struct device *dev = epap->drm.dev;
+
+	u16 pwr_opt[5] = {0x60a5, 0x89a5, 0x9000, 0x932a, 0x7341};
+	int idx, ret, i, foo;
+	int fps_min, fps_max;
+	u8 param;
+	int step = 0;
+
+	dev_dbg(dev, "Enabling gdepaper pipe\n");
+	if (!drm_dev_enter(pipe->crtc.dev, &idx))
+		return;
+
+	/* Reset and power on */
+	gdepaper_reset(epap);
+	ret = gdepaper_power_on(epap);
+	if (ret)
+		goto err_out;
+	usleep_range(1000, 20000); /* FIXME time this */
+
+	/* Basic controller setup (PSR) */
+	step = 1;
+	if (epap->controller_res < 0 ||
+	    epap->controller_res > 3) {
+		dev_err_once(dev, "Invalid controller resolution %d\n",
+				epap->controller_res);
+		goto err_out;
+	}
+	epap->psr = epap->controller_res<<6;
+	if (epap->display_colors == GDEPAPER_COL_BW)
+		epap->psr |= GDEP_PSR_COLOR_BW;
+
+	if (!epap->mirror_x)
+		epap->psr |= GDEP_PSR_SH_RIGHT;
+	if (!epap->mirror_y)
+		epap->psr |= GDEP_PSR_SCAN_UP;
+	epap->psr |= GDEP_PSR_BOOST_ON | GDEP_PSR_SOFT_RST;
+	ret = gdepaper_command(epap, GDEP_CMD_PANEL_SETUP,
+				   &epap->psr, sizeof(epap->psr));
+	if (ret)
+		goto err_out;
+
+	/* PLL setup */
+	step = 2;
+	/* The min/max values below were taken from the datasheet's PLL
+	 * coefficient table.
+	 */
+	switch (epap->pll_div) {
+	case 1:
+		param = 0;
+		fps_min = 68010;
+		fps_max = 197610;
+		break;
+	case 2:
+		param = 1;
+		fps_min = 34010;
+		fps_max = 120860;
+		break;
+	case 4:
+		param = 2;
+		fps_min = 20450;
+		fps_max = 60430;
+		break;
+	case 8:
+		param = 3;
+		fps_min = 20220;
+		fps_max = 30220;
+		break;
+	default:
+		dev_err_once(dev, "Invalid pll_div %d\n", epap->pll_div);
+		goto err_out;
+	}
+	if (epap->framerate_mHz < fps_min || epap->framerate_mHz > fps_max) {
+		dev_err_once(dev, "Framerate %d out of range for pll_div %d (%d-%d)\n",
+			     epap->framerate_mHz, epap->pll_div,
+			     fps_min, fps_max);
+		goto err_out;
+	}
+	/* The magic values below have been calculated through linear regression
+	 * on the framerate/PLL coefficient table from the controller datasheet.
+	 */
+	foo = (epap->framerate_mHz*1000 - (68014451 / epap->pll_div))
+		/ (2757368 * epap->pll_div);
+	if (foo < 0 || foo > 63) {
+		dev_err_once(dev, "PLL multiplier for framerate %d and pll_div %d out of range\n",
+				   epap->framerate_mHz, epap->pll_div);
+		goto err_out;
+	}
+	if (foo < 32)
+		foo = 63 - foo;
+	else
+		foo = foo - 32;
+	param |= foo;
+	ret = gdepaper_command(epap, GDEP_CMD_PLL_CTRL,
+				   &param, sizeof(param));
+	if (ret)
+		goto err_out;
+
+	/* Booster soft start configuration */
+	step = 3;
+	ret = gdepaper_command(epap, GDEP_CMD_BST_SOFT_START, epap->ss_param,
+		      sizeof(epap->ss_param));
+	if (ret)
+		goto err_out;
+
+	/* Undocumented "power optimization" command from reference code */
+	step = 4;
+	for (i = 0; i < ARRAY_SIZE(pwr_opt); i++) {
+		/* FIXME check in pulseview this endianness conversion works as
+		 * intended
+		 */
+		pwr_opt[i] = cpu_to_be16(pwr_opt[i]);
+		ret = gdepaper_command(epap, GDEP_CMD_MAGIC1,
+				       (u8 *)&pwr_opt[i], sizeof(pwr_opt[i]));
+		if (ret)
+			goto err_out;
+	}
+
+	/* Configure refresh-related parameters (LUTs, voltages, timings etc.)*/
+	step = 5;
+	ret = gdepaper_config_refresh(epap);
+	if (ret)
+		goto err_out;
+
+	/* Mysterious partial refresh call to finalize config */
+	step = 6;
+	/* This one-byte parameter format is used in the example code by
+	 * good display, but does not match the datasheet.
+	 */
+	param = 0x00;
+	ret = gdepaper_command(epap, GDEP_CMD_PART_DISP_RF,
+				   &param, sizeof(param));
+	if (ret)
+		goto err_out;
+
+	epap->enabled = true;
+
+	/* We need to make sure to power off the display to avoid damage */
+	ret = gdepaper_power_off(epap);
+	if (ret)
+		dev_err(dev, "Can't power off display, %d\n", ret);
+	drm_dev_exit(idx);
+	return;
+
+err_out:
+	dev_err(dev, "Error on pipe enable; ret=%d, step=%d\n", ret, step);
+	/* Try to turn off anyway */
+	gdepaper_power_off(epap);
+	drm_dev_exit(idx);
+}
+
+static void gdepaper_pipe_disable(struct drm_simple_display_pipe *pipe)
+{
+	struct gdepaper *epap = drm_to_gdepaper(pipe->crtc.dev);
+
+	dev_dbg(epap->drm.dev, "Disabling gdepaper pipe\n");
+	/* This callback is not protected by drm_dev_enter/exit since we want to
+	 * turn off the display on regular driver unload. It's highly unlikely
+	 * that the underlying SPI controller is gone should this be called
+	 * after unplug.
+	 */
+
+	if (!epap->enabled)
+		return;
+
+	/* Ignore errors */
+	gdepaper_power_off(epap);
+	gdepaper_enter_deep_sleep(epap);
+	epap->enabled = false;
+}
+
+static void gdepaper_pipe_update(struct drm_simple_display_pipe *pipe,
+				struct drm_plane_state *old_state)
+{
+	struct drm_plane_state *state = pipe->plane.state;
+	struct drm_crtc *crtc = &pipe->crtc;
+	struct drm_rect rect;
+
+	if (drm_atomic_helper_damage_merged(old_state, state, &rect))
+		gdepaper_fb_dirty(state->fb, &rect);
+
+	if (crtc->state->event) {
+		spin_lock_irq(&crtc->dev->event_lock);
+		drm_crtc_send_vblank_event(crtc, crtc->state->event);
+		spin_unlock_irq(&crtc->dev->event_lock);
+		crtc->state->event = NULL;
+	}
+}
+
+/* This function accepts if either all or no LUTs are given. */
+static int gdepaper_of_read_luts(struct gdepaper *epap, struct device_node *np,
+				 struct device *dev)
+{
+	int ret[5];
+
+	ret[0] = of_property_read_u8_array(np, "lut_vcom_dc",
+		epap->rfp.lut_vcom_dc, sizeof(epap->rfp.lut_vcom_dc));
+	ret[1] = of_property_read_u8_array(np, "lut_ww",
+		epap->rfp.lut_ww, sizeof(epap->rfp.lut_ww));
+	ret[2] = of_property_read_u8_array(np, "lut_wb",
+		epap->rfp.lut_wb, sizeof(epap->rfp.lut_wb));
+	ret[3] = of_property_read_u8_array(np, "lut_bw",
+		epap->rfp.lut_bw, sizeof(epap->rfp.lut_bw));
+	ret[4] = of_property_read_u8_array(np, "lut_bb",
+			epap->rfp.lut_bb, sizeof(epap->rfp.lut_bb));
+
+	/* All LUTs are given */
+	if (!ret[0] && !ret[1] && !ret[2] & !ret[3] && !ret[4]) {
+		epap->rfp.use_otp_luts_flag = 0;
+		return 0;
+	}
+
+	/* No LUTs are given */
+	if (ret[0] == -EINVAL && ret[1] == -EINVAL && ret[2] == -EINVAL &&
+			ret[3] == -EINVAL && ret[4] == -EINVAL) {
+		epap->rfp.use_otp_luts_flag = 1;
+		return 0;
+	}
+
+	dev_err(dev, "couldn't parse some LUTs - using to OTP LUTs: vcom_dc=%d ww=%d wb=%d bw=%d bb=%d\n",
+			ret[0], ret[1], ret[2], ret[3], ret[4]);
+	return -EINVAL;
+}
+
+static struct drm_display_mode *gdepaper_of_read_mode(
+	const struct gdepaper_type_descriptor *type,
+	struct device_node *np,
+	struct device *dev)
+{
+	u32 dims[4];
+	int ret1, ret2;
+	struct drm_display_mode *mode =
+		kzalloc(sizeof(struct drm_display_mode), GFP_KERNEL);
+	if (!mode)
+		return ERR_PTR(-ENOMEM);
+
+	ret1 = of_property_read_u32_array(np, "dimensions-px", &dims[0], 2);
+	ret2 = of_property_read_u32_array(np, "dimensions-mm", &dims[2], 2);
+
+	if (!ret1 && !ret2) {
+		*mode = (struct drm_display_mode){
+			DRM_SIMPLE_MODE(dims[0], dims[1], dims[2], dims[3]),
+		};
+
+	} else if (ret1 == -EINVAL && ret2 == -EINVAL) {
+		if (type) {
+			*mode = (struct drm_display_mode){
+				DRM_SIMPLE_MODE(type->w_px, type->h_px,
+						type->w_mm, type->h_mm)
+			};
+
+		} else {
+			DRM_DEV_ERROR(dev, "dimensions must be given\n");
+			kfree(mode);
+			return ERR_PTR(-EINVAL);
+		}
+
+	} else {
+		DRM_DEV_ERROR(dev, "invalid dimensions: %d/%d\n", ret1, ret2);
+		kfree(mode);
+		return ERR_PTR(ret1 || ret2);
+	}
+
+	return mode;
+}
+
+static const struct drm_simple_display_pipe_funcs gdepaper_pipe_funcs = {
+	.enable		= gdepaper_pipe_enable,
+	.disable	= gdepaper_pipe_disable,
+	.update		= gdepaper_pipe_update,
+	.prepare_fb	= drm_gem_fb_simple_display_pipe_prepare_fb,
+};
+
+DEFINE_DRM_GEM_CMA_FOPS(gdepaper_fops);
+
+int gdepaper_force_full_refresh_ioctl(struct drm_device *drm_dev, void *data,
+		struct drm_file *file)
+{
+	struct gdepaper *epap = drm_to_gdepaper(drm_dev);
+	int ret, idx;
+	/* FIXME same as below in update luts. locks? */
+
+	if (!drm_dev_enter(drm_dev, &idx))
+		/* FIXME is this the correct return code? Should we retry? */
+		return -EBUSY;
+
+	ret = gdepaper_wait_busy(epap);
+	if (ret)
+		goto out;
+
+	/* FIXME should we lock to sync against update here? */
+	ret = gdepaper_command(epap, GDEP_CMD_DISP_RF, NULL, 0);
+
+out:
+	drm_dev_exit(idx);
+	return ret;
+}
+
+int gdepaper_get_refresh_params_ioctl(struct drm_device *drm_dev, void *data,
+		struct drm_file *file)
+{
+	struct gdepaper *epap = drm_to_gdepaper(drm_dev);
+
+	return copy_to_user(data, &epap->rfp, sizeof(epap->rfp));
+}
+
+int gdepaper_set_refresh_params_ioctl(struct drm_device *drm_dev, void *data,
+		struct drm_file *file)
+{
+	struct gdepaper *epap = drm_to_gdepaper(drm_dev);
+	int ret, idx;
+
+	if (!drm_dev_enter(drm_dev, &idx)) {
+		/* FIXME is this the correct return code? Should we retry? */
+		ret = -EBUSY;
+		goto err_out;
+	}
+
+	if (copy_from_user(&epap->rfp, data, sizeof(epap->rfp))) {
+		ret = -EFAULT;
+		goto err_out;
+	}
+
+	ret = gdepaper_wait_busy(epap);
+	if (ret)
+		goto err_out;
+
+	/* FIXME should we lock to sync against update here? */
+	ret = gdepaper_config_refresh(epap);
+err_out:
+	drm_dev_exit(idx);
+	return ret;
+}
+
+int gdepaper_set_partial_update_en_ioctl(struct drm_device *drm_dev,
+		void *data, struct drm_file *file)
+{
+	struct gdepaper *epap = drm_to_gdepaper(drm_dev);
+	u32 *param = data;
+
+	epap->partial_update_en = *param;
+	return 0;
+}
+
+static const struct drm_ioctl_desc gdepaper_ioctls[] = {
+	DRM_IOCTL_DEF_DRV(GDEPAPER_FORCE_FULL_REFRESH,
+			gdepaper_force_full_refresh_ioctl,
+			DRM_AUTH),
+	DRM_IOCTL_DEF_DRV(GDEPAPER_SET_REFRESH_PARAMS,
+			gdepaper_set_refresh_params_ioctl,
+			DRM_AUTH | DRM_ROOT_ONLY),
+	DRM_IOCTL_DEF_DRV(GDEPAPER_GET_REFRESH_PARAMS,
+			gdepaper_get_refresh_params_ioctl,
+			DRM_AUTH),
+	DRM_IOCTL_DEF_DRV(GDEPAPER_SET_PARTIAL_UPDATE_EN,
+			gdepaper_set_partial_update_en_ioctl,
+			DRM_AUTH | DRM_ROOT_ONLY),
+};
+
+static void gdepaper_release(struct drm_device *drm)
+{
+	struct gdepaper *epap = drm_to_gdepaper(drm);
+
+	drm_mode_config_cleanup(drm);
+	drm_dev_fini(drm);
+	kfree(epap);
+}
+
+static struct drm_driver gdepaper_driver = {
+	.driver_features	= DRIVER_GEM | DRIVER_MODESET | DRIVER_PRIME |
+				  DRIVER_ATOMIC,
+	.fops			= &gdepaper_fops,
+	.release		= gdepaper_release,
+	DRM_GEM_CMA_VMAP_DRIVER_OPS,
+	.name			= "gdepaper",
+	.desc			= "Good Display ePaper panel",
+	.date			= "20190715",
+	.major			= 1,
+	.minor			= 0,
+	.ioctls			= gdepaper_ioctls,
+	.num_ioctls		= ARRAY_SIZE(gdepaper_ioctls),
+};
+
+static const uint32_t gdepaper_formats[] = {
+	DRM_FORMAT_RGB565,
+	DRM_FORMAT_XRGB8888,
+};
+
+static const struct drm_mode_config_funcs gdepaper_dbi_mode_config_funcs = {
+	.fb_create = drm_gem_fb_create_with_dirty,
+	.atomic_check = drm_atomic_helper_check,
+	.atomic_commit = drm_atomic_helper_commit,
+};
+
+#include "gdepaper_models.h"
+MODULE_DEVICE_TABLE(of, gdepaper_of_match);
+
+static const struct spi_device_id gdepaper_spi_id[] = {
+	{"epaper", 0},
+	{}
+};
+MODULE_DEVICE_TABLE(spi, gdepaper_spi_id);
+
+static int gdepaper_probe(struct spi_device *spi)
+{
+	struct device *dev = &spi->dev;
+	struct device_node *np = dev->of_node;
+	const struct of_device_id *of_id;
+	struct drm_device *drm;
+	struct drm_display_mode *mode;
+	struct gdepaper *epap;
+	const struct gdepaper_type_descriptor *type_desc;
+	int ret;
+	size_t bufsize;
+
+	of_id = of_match_node(gdepaper_of_match, np);
+	if (WARN_ON(of_id == NULL)) {
+		dev_warn(dev, "dt node didn't match, aborting probe\n");
+		return -EINVAL;
+	}
+	type_desc = of_id->data;
+
+	dev_dbg(dev, "Probing gdepaper module\n");
+	epap = kzalloc(sizeof(*epap), GFP_KERNEL);
+	if (!epap)
+		return -ENOMEM;
+
+	epap->enabled = false;
+	mutex_init(&epap->cmdlock);
+	epap->tx_buf = NULL;
+	epap->spi = spi;
+
+	drm = &epap->drm;
+	ret = devm_drm_dev_init(dev, drm, &gdepaper_driver);
+	if (ret) {
+		dev_warn(dev, "failed to init drm dev\n");
+		goto err_free;
+	}
+
+	drm_mode_config_init(drm);
+
+	epap->reset = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH);
+	if (IS_ERR(epap->reset)) {
+		dev_err(dev, "Failed to get reset GPIO\n");
+		ret = PTR_ERR(epap->reset);
+		goto err_free;
+	}
+
+	epap->busy = devm_gpiod_get(dev, "busy", GPIOD_IN);
+	if (IS_ERR(epap->busy)) {
+		dev_err(dev, "Failed to get busy GPIO\n");
+		ret = PTR_ERR(epap->busy);
+		goto err_free;
+	}
+
+	epap->dc = devm_gpiod_get(dev, "dc", GPIOD_OUT_LOW);
+	if (IS_ERR(epap->dc)) {
+		dev_err(dev, "Failed to get dc GPIO\n");
+		ret = PTR_ERR(epap->dc);
+		goto err_free;
+	}
+
+	epap->spi_speed_hz = 2000000;
+	epap->pll_div = 1;
+	epap->framerate_mHz = 81850;
+	epap->rfp.vg_lv = GDEP_PWR_VGHL_16V;
+	epap->rfp.vcom_sel = 0;
+	epap->rfp.vdh_bw_mv = 11000; /* drive high level, b/w pixel */
+	epap->rfp.vdh_col_mv = 4200; /* drive high level, red/yellow pixel */
+	epap->rfp.vdl_mv = -11000; /* drive low level */
+	epap->rfp.border_data_sel = 2; /* "vbd" */
+	epap->rfp.data_polarity = 0; /* "ddx" */
+	epap->rfp.vcom_dc_mv = -1000;
+	epap->rfp.vcom_data_ivl_hsync = 10; /* hsync periods */
+	epap->rfp.use_otp_luts_flag = 1;
+	epap->ss_param[0] = 0x07;
+	epap->ss_param[1] = 0x07;
+	epap->ss_param[2] = 0x17;
+	epap->controller_res = GDEP_CTRL_RES_320X300;
+
+	ret = gdepaper_of_read_luts(epap, np, dev);
+	if (ret) {
+		dev_warn(dev, "can't read LUTs from dt\n");
+		goto err_free;
+	}
+
+	of_property_read_u32(np, "controller-resolution",
+			&epap->controller_res);
+	of_property_read_u32(np, "spi-speed-hz", &epap->spi_speed_hz);
+	epap->partial_update_en = of_property_read_bool(np, "partial-update");
+	ret = of_property_read_u32(np, "colors", &epap->display_colors);
+	if (ret == -EINVAL) {
+		if (type_desc) {
+			epap->display_colors = type_desc->colors;
+
+		} else {
+			dev_err(dev, "colors must be set in dt\n");
+			ret = -EINVAL;
+			goto err_free;
+		}
+	} else if (ret) {
+		dev_err(dev, "Invalid dt colors property\n");
+		goto err_free;
+	}
+	if (epap->display_colors < 0 ||
+			epap->display_colors >= GDEPAPER_COL_END) {
+		dev_err(dev, "invalid colors value\n");
+		ret = -EINVAL;
+		goto err_free;
+	}
+	epap->mirror_x = of_property_read_bool(np, "mirror-x");
+	epap->mirror_y = of_property_read_bool(np, "mirror-y");
+	of_property_read_u32(np, "pll-div", &epap->pll_div);
+	of_property_read_u32(np, "fps-millihertz", &epap->framerate_mHz);
+	of_property_read_u32(np, "vghl-level", &epap->rfp.vg_lv);
+	epap->vds_en = !of_property_read_bool(np, "vds-external");
+	epap->vdg_en = !of_property_read_bool(np, "vdg-external");
+	of_property_read_u32(np, "vcom", &epap->rfp.vcom_sel);
+	of_property_read_u32(np, "vdh-bw-millivolts", &epap->rfp.vdh_bw_mv);
+	of_property_read_u32(np, "vdh-color-millivolts", &epap->rfp.vdh_col_mv);
+	of_property_read_u32(np, "vdl-millivolts", &epap->rfp.vdl_mv);
+	of_property_read_u32(np, "border-data", &epap->rfp.border_data_sel);
+	of_property_read_u32(np, "data-polarity", &epap->rfp.data_polarity);
+	ret = of_property_read_u8_array(np, "boost-soft-start",
+			(u8 *)&epap->ss_param, sizeof(epap->ss_param));
+	if (ret && ret != -EINVAL)
+		dev_err(dev, "invalid boost-soft-start value, ignoring\n");
+	of_property_read_u32(np, "vcom-data-interval-periods",
+			&epap->rfp.vcom_data_ivl_hsync);
+
+	/* Accept both positive and negative notation */
+	if (epap->rfp.vdl_mv < 0)
+		epap->rfp.vdl_mv = -epap->rfp.vdl_mv;
+	if (epap->rfp.vcom_dc_mv < 0)
+		epap->rfp.vcom_dc_mv = -epap->rfp.vcom_dc_mv;
+
+	/* (from mipi-dbi.c:)
+	 * Even though it's not the SPI device that does DMA (the master does),
+	 * the dma mask is necessary for the dma_alloc_wc() in
+	 * drm_gem_cma_create(). The dma_addr returned will be a physical
+	 * address which might be different from the bus address, but this is
+	 * not a problem since the address will not be used.
+	 * The virtual address is used in the transfer and the SPI core
+	 * re-maps it on the SPI master device using the DMA streaming API
+	 * (spi_map_buf()).
+	 */
+	if (!dev->coherent_dma_mask) {
+		ret = dma_coerce_mask_and_coherent(dev, DMA_BIT_MASK(32));
+		if (ret) {
+			dev_warn(dev, "Failed to set dma mask %d\n", ret);
+			goto err_free;
+		}
+	}
+
+	mode = gdepaper_of_read_mode(type_desc, np, dev);
+	if (IS_ERR(mode)) {
+		dev_warn(dev, "Failed to read mode: %ld\n", PTR_ERR(mode));
+		ret = PTR_ERR(mode);
+		goto err_free;
+	}
+
+	/* 8 pixels per byte, bit-packed */
+	bufsize = (mode->vdisplay * mode->hdisplay + 7)/8;
+	epap->tx_buf = devm_kmalloc(drm->dev, bufsize, GFP_KERNEL);
+	if (!epap->tx_buf) {
+		ret = -ENOMEM;
+		goto err_free;
+	}
+
+	/* TODO rotation support? */
+	ret = tinydrm_display_pipe_init(drm, &epap->pipe, &gdepaper_pipe_funcs,
+					DRM_MODE_CONNECTOR_VIRTUAL,
+					gdepaper_formats,
+					ARRAY_SIZE(gdepaper_formats), mode, 0);
+	if (ret) {
+		dev_warn(dev, "Failed to initialize display pipe: %d\n", ret);
+		goto err_free;
+	}
+
+	drm->mode_config.funcs = &gdepaper_dbi_mode_config_funcs;
+	drm->mode_config.preferred_depth = 32;
+	drm_plane_enable_fb_damage_clips(&epap->pipe.plane);
+	drm_mode_config_reset(drm);
+
+	ret = drm_dev_register(drm, 0);
+	if (ret) {
+		dev_warn(dev, "Failed to register drm device: %d\n", ret);
+		goto err_free;
+	}
+
+	spi_set_drvdata(spi, drm);
+	drm_fbdev_generic_setup(drm, 0);
+
+	dev_dbg(dev, "Probed gdepaper module\n");
+	return 0;
+err_free:
+	kfree(epap);
+	return ret;
+}
+
+static int gdepaper_remove(struct spi_device *spi)
+{
+	struct drm_device *drm = spi_get_drvdata(spi);
+
+	dev_dbg(drm->dev, "Removing gdepaper module\n");
+	drm_dev_unplug(drm);
+	drm_atomic_helper_shutdown(drm);
+
+	return 0;
+}
+
+static void gdepaper_shutdown(struct spi_device *spi)
+{
+	struct drm_device *drm = spi_get_drvdata(spi);
+
+	dev_dbg(drm->dev, "Shutting down gdepaper module\n");
+	drm_atomic_helper_shutdown(spi_get_drvdata(spi));
+}
+
+static struct spi_driver gdepaper_spi_driver = {
+	.driver = {
+		.name = "gdepaper",
+		.owner = THIS_MODULE,
+		.of_match_table = gdepaper_of_match,
+	},
+	.id_table = gdepaper_spi_id,
+	.probe = gdepaper_probe,
+	.remove = gdepaper_remove,
+	.shutdown = gdepaper_shutdown,
+};
+module_spi_driver(gdepaper_spi_driver);
+
+MODULE_DESCRIPTION("Good Display epaper panel driver");
+MODULE_AUTHOR("Jan Sebastian Götte <linux@jaseg.net>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/gpu/drm/tinydrm/gdepaper_models.h b/drivers/gpu/drm/tinydrm/gdepaper_models.h
new file mode 100644
index 000000000000..1b9125d711de
--- /dev/null
+++ b/drivers/gpu/drm/tinydrm/gdepaper_models.h
@@ -0,0 +1,157 @@ 
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * This file contains a list of Good Display epaper display model numbers
+ * listing their color mode (B/W, B/W/Red, B/W/Yellow) as well as their physical
+ * dimensions and resolution.
+ *
+ * This data was manually extracted from the displays' specifications pages on
+ * the mfg website.
+ *
+ * The following displays use third-party driver chips with command sets this
+ * driver can't handle:
+ *   GDEH0154D67
+ *   GDEH0213B73
+ *   GDE060B3
+ *   GDE060F3
+ *   GDE060BA, GDE060BAT, GDE060BAFL GDE060BAFLT
+ *
+ * To add a display, insert the appropriate descriptor line below, then link it
+ * into the table at the bottom of this file. This driver should handle any
+ * Good Display driver chip (IL*, GD*) with minor modifications.
+ *
+ * Copyright 2019 Jan Sebastian Goette
+ */
+
+#define DEF_EPD_TYPE(type, col, w_mm, h_mm, w_px, h_px) \
+	static const struct gdepaper_type_descriptor gddsp_ ## type ## _data = \
+	{col, w_mm, h_mm, w_px, h_px}
+
+DEF_EPD_TYPE(gdew0154z04t,    GDEPAPER_COL_BW_RED,     28,  28,  200, 200); /*    1.54"    b/w/r  IL0376F     */
+DEF_EPD_TYPE(gdew0154z04fl,   GDEPAPER_COL_BW_RED,     28,  28,  200, 200); /*    1.54"    b/w/r  IL0376F     */
+DEF_EPD_TYPE(gdew0154z04,     GDEPAPER_COL_BW_RED,     28,  28,  200, 200); /*    1.54"    b/w/r  IL0376F     */
+DEF_EPD_TYPE(gdewl0154z17fl,  GDEPAPER_COL_BW_RED,     28,  28,  152, 152); /*    1.54"    b/w/r  IL0373      */
+DEF_EPD_TYPE(gdew0154z17,     GDEPAPER_COL_BW_RED,     28,  28,  152, 152); /*    1.54"    b/w/r  IL0373      */
+DEF_EPD_TYPE(gdew0154c39fl,   GDEPAPER_COL_BW_YELLOW,  28,  28,  152, 152); /*    1.54"    b/w/y              */
+DEF_EPD_TYPE(gdew0154c39,     GDEPAPER_COL_BW_YELLOW,  28,  28,  152, 152); /*    1.54"    b/w/y              */
+DEF_EPD_TYPE(gdew0213z16,     GDEPAPER_COL_BW_RED,     49,  24,  212, 104); /*    2.13"    b/w/r  IL0373      */
+DEF_EPD_TYPE(gdew0213c38,     GDEPAPER_COL_BW_YELLOW,  49,  24,  212, 104); /*    2.13"    b/w/y  IL0373      */
+DEF_EPD_TYPE(gdew026z39,      GDEPAPER_COL_BW_RED,     60,  31,  296, 152); /*    2.6"     b/w/r  IL0373      */
+DEF_EPD_TYPE(gdew027c44t,     GDEPAPER_COL_BW_RED,     57,  38,  264, 176); /*    2.7"     b/w/r  IL91874     */
+DEF_EPD_TYPE(gdew027c44,      GDEPAPER_COL_BW_RED,     57,  38,  264, 176); /*    2.7"     b/w/r  IL91874     */
+DEF_EPD_TYPE(gdew029z10,      GDEPAPER_COL_BW_RED,     67,  29,  296, 128); /*    2.9"     b/w/r  IL0373      */
+DEF_EPD_TYPE(gdew029c32,      GDEPAPER_COL_BW_YELLOW,  67,  29,  296, 128); /*    2.9"     b/w/y  IL0373      */
+DEF_EPD_TYPE(gdew0371z80,     GDEPAPER_COL_BW_RED,     82,  47,  416, 240); /*    3.71"    b/w/r  GD8102      */
+DEF_EPD_TYPE(gdew042z15,      GDEPAPER_COL_BW_RED,     85,  64,  400, 300); /*    4.2"     b/w/r  IL0398      */
+DEF_EPD_TYPE(gdew042c37,      GDEPAPER_COL_BW_YELLOW,  85,  64,  400, 300); /*    4.2"     b/w/y  IL0398      */
+DEF_EPD_TYPE(gdew0583c64,     GDEPAPER_COL_BW_YELLOW, 119,  88,  600, 448); /*    5.83"    b/w/y  IL0371      */
+DEF_EPD_TYPE(gdew0583z21,     GDEPAPER_COL_BW_RED,    119,  88,  600, 448); /*    5.83"    b/w/r  IL0371      */
+DEF_EPD_TYPE(gdew075z08,      GDEPAPER_COL_BW_RED,    163,  98,  800, 480); /*    7.5"     b/w/r  GD7965      */
+DEF_EPD_TYPE(gdew075z09,      GDEPAPER_COL_BW_RED,    163,  98,  640, 384); /*    7.5"     b/w/r  IL0371      */
+DEF_EPD_TYPE(gdew075c21,      GDEPAPER_COL_BW_YELLOW, 163,  98,  640, 384); /*    7.5"     b/w/y  IL0371      */
+/* These are not yet supported since they use multiple driver chips in cascade mode */
+/* DEF_EPD_TYPE(gdew1248z95,     GDEPAPER_COL_BW_RED,    253, 191, 1304, 984); */ /*   12.48"    b/w/r  IL0326 (4x) */
+/* DEF_EPD_TYPE(gdew1248c63,     GDEPAPER_COL_BW_YELLOW, 253, 191, 1304, 984); */ /*   12.48"    b/w/y  IL0326 (4x) */
+/* DEF_EPD_TYPE(gdew1248t3,      GDEPAPER_COL_BW,        253, 191, 1304, 984); */ /*   12.48"    b/w    IL0326 (4x) */
+DEF_EPD_TYPE(gdew0102i4f,     GDEPAPER_COL_BW,         22,  14,  128,  80); /*    1.02"    b/w    IL0323      */
+DEF_EPD_TYPE(gdew0102i4fc,    GDEPAPER_COL_BW,         22,  14,  128,  80); /*    1.02"    b/w                */
+DEF_EPD_TYPE(gdep014tt1,      GDEPAPER_COL_BW,         14,  33,  128, 296); /*    1.43"    b/w                */
+DEF_EPD_TYPE(gdep015oc1,      GDEPAPER_COL_BW,         28,  28,  200, 200); /*    1.54"    b/w    IL3829      */
+DEF_EPD_TYPE(gdew0154t8t,     GDEPAPER_COL_BW,         28,  28,  152, 152); /*    1.54"    b/w    IL0373      */
+DEF_EPD_TYPE(gdew0154t8fl,    GDEPAPER_COL_BW,         28,  28,  152, 152); /*    1.54"    b/w    IL0373      */
+DEF_EPD_TYPE(gdew0154t8,      GDEPAPER_COL_BW,         28,  28,  152, 152); /*    1.54"    b/w    IL0373      */
+DEF_EPD_TYPE(gdeh0154d27t,    GDEPAPER_COL_BW,         28,  28,  200, 200); /*    1.54"    b/w                */
+DEF_EPD_TYPE(gdem0154e97lt,   GDEPAPER_COL_BW,         28,  28,  152, 152); /*    1.54"    b/w    IL3895      */
+DEF_EPD_TYPE(gdew0154i9f,     GDEPAPER_COL_BW,         28,  28,  152, 152); /*    1.54"    b/w                */
+DEF_EPD_TYPE(gdew0213t5,      GDEPAPER_COL_BW,         24,  49,  212, 104); /*    2.13"    b/w    IL0373      */
+DEF_EPD_TYPE(gdew0213i5f,     GDEPAPER_COL_BW,         49,  24,  212, 104); /*    2.13"    b/w                */
+DEF_EPD_TYPE(gdeh0213d30lt,   GDEPAPER_COL_BW,         23,  48,  212, 104); /*    2.13"    b/w    IL3820      */
+DEF_EPD_TYPE(gdew0213v7lt,    GDEPAPER_COL_BW,         24,  49,  212, 104); /*    2.13"    b/w    IL0373      */
+DEF_EPD_TYPE(gdth0213zhft34,  GDEPAPER_COL_BW,         24,  49,  250, 122); /*    2.13"    b/w                */
+DEF_EPD_TYPE(gdew026t0,       GDEPAPER_COL_BW,         60,  31,  296, 152); /*    2.6"     b/w    IL0373      */
+DEF_EPD_TYPE(gdew027w3t,      GDEPAPER_COL_BW,         57,  38,  264, 176); /*    2.7"     b/w    IL91874     */
+DEF_EPD_TYPE(gdew027w3,       GDEPAPER_COL_BW,         57,  38,  264, 176); /*    2.7"     b/w    IL91874     */
+DEF_EPD_TYPE(gdeh029a1,       GDEPAPER_COL_BW,         67,  29,  296, 128); /*    2.9"     b/w    IL3820      */
+DEF_EPD_TYPE(gdew029t5,       GDEPAPER_COL_BW,         67,  29,  296, 128); /*    2.9"     b/w    IL0373      */
+DEF_EPD_TYPE(gdeh029d56lt,    GDEPAPER_COL_BW,         67,  29,  296, 128); /*    2.9"     b/w                */
+DEF_EPD_TYPE(gdew029i6f,      GDEPAPER_COL_BW,         67,  29,  296, 128); /*    2.9"     b/w                */
+DEF_EPD_TYPE(gdew0371w7,      GDEPAPER_COL_BW,         82,  47,  416, 240); /*    3.71"    b/w    GP8102      */
+DEF_EPD_TYPE(gdew042t2,       GDEPAPER_COL_BW,         83,  64,  400, 300); /*    4.2"     b/w    IL0398      */
+DEF_EPD_TYPE(gdep043zf3,      GDEPAPER_COL_BW,         56,  94,  800, 480); /*    4.3"     b/w                */
+DEF_EPD_TYPE(gde043a2t,       GDEPAPER_COL_BW,         88,  66,  800, 600); /*    4.3"     b/w                */
+DEF_EPD_TYPE(gde043a2,        GDEPAPER_COL_BW,         88,  66,  800, 600); /*    4.3"     b/w                */
+DEF_EPD_TYPE(gdew0583t7,      GDEPAPER_COL_BW,        119,  88,  600, 448); /*    5.83"    b/w    IL0371      */
+DEF_EPD_TYPE(gdew075t8,       GDEPAPER_COL_BW,        163,  98,  640, 384); /*    7.5"     b/w    IL0371      */
+DEF_EPD_TYPE(gdew075t7,       GDEPAPER_COL_BW,        163,  98,  800, 480); /*    7.5"     b/w    GD7965      */
+DEF_EPD_TYPE(gdew080t5,       GDEPAPER_COL_BW,        122, 163, 1024, 768); /*    8"       b/w                */
+DEF_EPD_TYPE(gdep097tc2,      GDEPAPER_COL_BW,        203, 139, 1200, 825); /*    9.7"     b/w                */
+
+#undef DEF_EPD_TYPE
+
+#define EPD_OF_ENTRY(type) \
+	{ .compatible = "gooddisplay," #type, .data = &gddsp_ ## type ## _data }
+
+static const struct of_device_id gdepaper_of_match[] = {
+	{ .compatible = "gooddisplay,generic_epaper",	.data = NULL },
+	EPD_OF_ENTRY(gdew0154z04t),
+	EPD_OF_ENTRY(gdew0154z04fl),
+	EPD_OF_ENTRY(gdew0154z04),
+	EPD_OF_ENTRY(gdewl0154z17fl),
+	EPD_OF_ENTRY(gdew0154z17),
+	EPD_OF_ENTRY(gdew0154c39fl),
+	EPD_OF_ENTRY(gdew0154c39),
+	EPD_OF_ENTRY(gdew0213z16),
+	EPD_OF_ENTRY(gdew0213c38),
+	EPD_OF_ENTRY(gdew026z39),
+	EPD_OF_ENTRY(gdew027c44t),
+	EPD_OF_ENTRY(gdew027c44),
+	EPD_OF_ENTRY(gdew029z10),
+	EPD_OF_ENTRY(gdew029c32),
+	EPD_OF_ENTRY(gdew0371z80),
+	EPD_OF_ENTRY(gdew042z15),
+	EPD_OF_ENTRY(gdew042c37),
+	EPD_OF_ENTRY(gdew0583c64),
+	EPD_OF_ENTRY(gdew0583z21),
+	EPD_OF_ENTRY(gdew075z08),
+	EPD_OF_ENTRY(gdew075z09),
+	EPD_OF_ENTRY(gdew075c21),
+	/* see comment above. */
+	/* EPD_OF_ENTRY(gdew1248z95), */
+	/* EPD_OF_ENTRY(gdew1248c63), */
+	/* EPD_OF_ENTRY(gdew1248t3), */
+	EPD_OF_ENTRY(gdew0102i4f),
+	EPD_OF_ENTRY(gdew0102i4fc),
+	EPD_OF_ENTRY(gdep014tt1),
+	EPD_OF_ENTRY(gdep015oc1),
+	EPD_OF_ENTRY(gdew0154t8t),
+	EPD_OF_ENTRY(gdew0154t8fl),
+	EPD_OF_ENTRY(gdew0154t8),
+	EPD_OF_ENTRY(gdeh0154d27t),
+	EPD_OF_ENTRY(gdem0154e97lt),
+	EPD_OF_ENTRY(gdew0154i9f),
+	EPD_OF_ENTRY(gdew0213t5),
+	EPD_OF_ENTRY(gdew0213i5f),
+	EPD_OF_ENTRY(gdeh0213d30lt),
+	EPD_OF_ENTRY(gdew0213v7lt),
+	EPD_OF_ENTRY(gdth0213zhft34),
+	EPD_OF_ENTRY(gdew026t0),
+	EPD_OF_ENTRY(gdew027w3t),
+	EPD_OF_ENTRY(gdew027w3),
+	EPD_OF_ENTRY(gdeh029a1),
+	EPD_OF_ENTRY(gdew029t5),
+	EPD_OF_ENTRY(gdeh029d56lt),
+	EPD_OF_ENTRY(gdew029i6f),
+	EPD_OF_ENTRY(gdew0371w7),
+	EPD_OF_ENTRY(gdew042t2),
+	EPD_OF_ENTRY(gdep043zf3),
+	EPD_OF_ENTRY(gde043a2t),
+	EPD_OF_ENTRY(gde043a2),
+	EPD_OF_ENTRY(gdew0583t7),
+	EPD_OF_ENTRY(gdew075t8),
+	EPD_OF_ENTRY(gdew075t7),
+	EPD_OF_ENTRY(gdew080t5),
+	EPD_OF_ENTRY(gdep097tc2),
+	{}
+};
+
+#undef EPD_OF_ENTRY
+