summaryrefslogtreecommitdiffstats
path: root/drivers/gpu/drm/tilcdc
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-09-13 12:27:00 -0700
committerLinus Torvalds <torvalds@linux-foundation.org>2026-09-13 12:27:00 -0700
commit22098763a10d9c1340827fcf6edab66f153b27f0 (patch)
tree3962f95a34df034652ff93686cdd156c7197f2d9 /drivers/gpu/drm/tilcdc
downloadlinux-stable-22098763a10d9c1340827fcf6edab66f153b27f0.tar.gz
linux-stable-22098763a10d9c1340827fcf6edab66f153b27f0.zip
Merge tag 'trace-v7.3-rc2' of git://git.kernel.org/pub/scm/linux/kernel/git/trace/linux-tracegrafted
Pull tracing fixes from Steven Rostedt: - Don't destroy user event fields when removal fails User event fields are destroyed before the event is removed from visibility. But that can fail leaving the still visible event with no fields. Move the destroying of the fields to after the event is successfully removed from visibility. - Initialize function graph state is fork before calling copy_exec_state() For non-CLONE_VM forks, copy_exec_state() allocates a new task_exec_state. If that allocation fails, ftrace_graph_exit_task() will free the tasks ret_stack pointer. Since that pointer is still using the parent's ret_stack, it mistakenly frees the parent's pointer too. Call ftrace_graph_init() on the task first which will NULL out the new tasks's ret_stack and if the copy fails, it will not free anything. - Remove FGRAPH_MAX_INDEX The macro FGRAPH_MAX_INDEX was added but never used. Remove it. - Save ent_size in function graph printing of nested functions The function graph tracer needs to look at the next event to see if the next event is the return of the current function entry. If it is, it prints a single line: ktime_get(); Otherwise it prints it like a nested function: tick_nohz_irq_exit() { ktime_get(); kcpustat_irq_exit(); } In order to look at the next event, it must save the current event so that it has the information to print from it. It saves the event in the iterator descriptor called "ent". What it doesn't save is the ent_size of the event which is now used to know if the function graph arguments are to be printed. The peek doesn't save the size so the size used happens to be that of the size of the last event that was seen. Save the entry event size in the iterator descriptor so that the correct size is used. - Fix several errors with freeing data in the histogram code The histogram code had a lot of leaked or or incorrect accounting when failures happen. Correct them. - Fix histogram regression of .percent and .graph modifiers Up until 6.3 histogram values could have "percent" or "graph" modifiers that changed how they were printed. But a change that added restricting histograms values from being strings, stack traces and other modifiers inadvertently prevented them from using the percent and graph modifiers, which were legal use cases for values. Put back the percent and graph modifiers. - Fix various typos in the comments - Set the trace_clock before initializing a histogram with clock argument The histogram API allows the user to specific which trace clock to use via a "clock=" string. The histogram is set up first before the clock is checked. If the passed in clock is not valid, it exits without fully fixing up the histogram leaving it on the list and a use-after-free can trigger. Update the clock argument first and if it fails then exit gracefully before the histogram trigger is placed on any lists. - Restore :mod: trailer after parsing in ftrace_set_clr_event The function ftrace_set_clr_event() modifies the parse string and needs to put it back to what was passed in. It searches for ":mod:" via a strsep() but fails to put back the first ':' in the string. Add back the ':' in the passed in string. - Take trace_array reference when opening a tracer options file The options files are dynamically created and some tracers add their own options. When a tracer adds their own list of options, the trace_array holding them has an array to hold the list of options for each tracer. This array increases in size via a krealloc(), and the new entry gets a newly allocated array to hold the options of the new tracer being added. The element in each entry of the tracer's option array holds a pointer back to the trace_array, a pointer to the tracer it is associated to, a pointer to the flags of the option. The issue is that these arrays are freed when the trace_array is freed when its instance it represents is removed from the instances directory. There's a race that an open of one of these options files can happen when the instance is being removed. Add a new helper function to be called by the open function of the options file to iterate all existing trace_arrays under a lock and find the one that has the given option element in one of it's tracer arrays. If found, then update the associated trace_array's reference counter to keep it from being freed. If not found, have the open call return -ENODEV. - Disable interrupts when acquiring the lock in rb_wake_up_waiters() The function rb_wake_up_waiters() assumes it will be called in interrupt context and does not disable irqs when taking cpu_buffer->reader_lock, which can be called in hard interrupt context. The issue is in PREEMPT_RT, this function is called in thread context leaving this lock open to a deadlock. Take the lock with interrupts disabled. - Use rcu_assign_pointer() for tmp_ops filter hash The tmp_ops used in update_ftrace_direct_mod() assigns its filter_hash field directly, but that field is annotated as __rcu and sparse complains. Assign it with rcu_assign_pointer() - Fix use-after-free in enable_trigger_private_data_free() The trace_event_call is accessed through the event_trigger_data's trace_event_file pointer to put the trace_event_call on freeing. The issue is that the trace_event_file data may have been freed already causing a use-after-free. Add a field to the event_trigger_data that points directly to the trace_event_call so that it can decrement its reference directly without needing to go through the trace_event_file. - Fix accounting of buffer data remote headers trace_buffer_desc_size() and trace_remote_alloc_buffer() undercount the number of pages is needed for the asked for size as it doesn't take into account the meta data on each page. Add a helper function to do the calculation properly and use that in these functions. - Catch nr_page_va overflow in ring_buffer_desc sizing The number of pages per remote ring buffer is capped by ring_buffer_desc::nr_page_va (32 bits). A buffer_size large enough to overflow that field would silently allocate a descriptor smaller than what was asked for. - Do not resize the subbuf order if any per_cpu buffer is disabled The mmapping of ring buffers disables resizing the subbuffers, but it is done per-cpu whereas the subbuf size change is done for all the per_cpu buffers under the buffer->mutex. It could change the size of some while the mapping is happening on others. Have the resize of the subbuf order check all the per_cpu buffers under the lock to see if any of them is disabled before starting and causing an inconsistency between buffers that are being mapped. * tag 'trace-v7.3-rc2' of git://git.kernel.org/pub/scm/linux/kernel/git/trace/linux-trace: (25 commits) ring-buffer: Check resize_disabled before publishing the new subbuf order tracing/remotes: Catch nr_page_va overflow in ring_buffer_desc sizing tracing/remotes: Account for ring buffer page header in size calculation tracing: Don't dereference trace_event_file in deferred trigger free ftrace: Use rcu_assign_pointer() for tmp_ops filter hash ring-buffer: Acquire the lock with irqsave in rb_wake_up_waiters() tracing: Take trace_array reference when opening a tracer options file tracing: Fix ring_buffer_read_page_size() kernel-doc tracing: Restore :mod: trailer after parsing in ftrace_set_clr_event() tracing: Fix memory corruption from a "STACKTRACE" histogram key tracing: Fix memory corruption from the histogram stacktrace modifier tracing: Undo the registration when enabling the histogram trigger fails tracing: Take the reference before publishing the named histogram trigger tracing: Set the trace clock before registering the histogram trigger tracing: Fix typo "preceeded" in comment tracing: Fix typo "availabe" in comment tracing: Let histogram values keep the percent and graph modifiers tracing: Keep the entry count when the histogram stats allocation fails tracing: Free histogram the field rejected for a bad modifier tracing: Free histogram the var ref when its initialization fails ...
Diffstat (limited to 'drivers/gpu/drm/tilcdc')
-rw-r--r--drivers/gpu/drm/tilcdc/Kconfig34
-rw-r--r--drivers/gpu/drm/tilcdc/Makefile14
-rw-r--r--drivers/gpu/drm/tilcdc/tilcdc_crtc.c1009
-rw-r--r--drivers/gpu/drm/tilcdc/tilcdc_drv.c501
-rw-r--r--drivers/gpu/drm/tilcdc/tilcdc_drv.h108
-rw-r--r--drivers/gpu/drm/tilcdc/tilcdc_encoder.c63
-rw-r--r--drivers/gpu/drm/tilcdc/tilcdc_encoder.h11
-rw-r--r--drivers/gpu/drm/tilcdc/tilcdc_panel_legacy.c185
-rw-r--r--drivers/gpu/drm/tilcdc/tilcdc_panel_legacy.dtso29
-rw-r--r--drivers/gpu/drm/tilcdc/tilcdc_plane.c116
-rw-r--r--drivers/gpu/drm/tilcdc/tilcdc_regs.h173
11 files changed, 2243 insertions, 0 deletions
diff --git a/drivers/gpu/drm/tilcdc/Kconfig b/drivers/gpu/drm/tilcdc/Kconfig
new file mode 100644
index 000000000..89df75287
--- /dev/null
+++ b/drivers/gpu/drm/tilcdc/Kconfig
@@ -0,0 +1,34 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config DRM_TILCDC
+ tristate "DRM Support for TI LCDC Display Controller"
+ depends on DRM && OF && ARM
+ select DRM_CLIENT_SELECTION
+ select DRM_KMS_HELPER
+ select DRM_GEM_DMA_HELPER
+ select DRM_BRIDGE
+ select DRM_DISPLAY_HELPER
+ select DRM_BRIDGE_CONNECTOR
+ select DRM_PANEL_BRIDGE
+ select VIDEOMODE_HELPERS
+ select BACKLIGHT_CLASS_DEVICE
+ help
+ Choose this option if you have an TI SoC with LCDC display
+ controller, for example AM33xx in beagle-bone, DA8xx, or
+ OMAP-L1xx. This driver replaces the FB_DA8XX fbdev driver.
+
+config DRM_TILCDC_PANEL_LEGACY
+ bool "Support device tree blobs using TI LCDC Panel binding"
+ default y
+ depends on DRM_TILCDC
+ depends on OF
+ depends on BACKLIGHT_CLASS_DEVICE
+ depends on PM
+ select OF_OVERLAY
+ select DRM_PANEL_SIMPLE
+ help
+ Modifies the live device tree at early boot to convert the legacy
+ "ti,tilcdc,panel" devicetree node to the standard panel-dpi node.
+ This allows to maintain backward compatibility for boards which
+ were using the deprecated tilcdc_panel driver.
+ If you find "ti,tilcdc,panel"-string from your DTB, you probably
+ need this. Otherwise you do not.
diff --git a/drivers/gpu/drm/tilcdc/Makefile b/drivers/gpu/drm/tilcdc/Makefile
new file mode 100644
index 000000000..c6b484dad
--- /dev/null
+++ b/drivers/gpu/drm/tilcdc/Makefile
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: GPL-2.0
+ifeq (, $(findstring -W,$(KCFLAGS)))
+ ccflags-y += -Werror
+endif
+
+tilcdc-y := \
+ tilcdc_plane.o \
+ tilcdc_crtc.o \
+ tilcdc_encoder.o \
+ tilcdc_drv.o
+
+obj-$(CONFIG_DRM_TILCDC) += tilcdc.o
+obj-$(CONFIG_DRM_TILCDC_PANEL_LEGACY) += tilcdc_panel_legacy.o \
+ tilcdc_panel_legacy.dtbo.o
diff --git a/drivers/gpu/drm/tilcdc/tilcdc_crtc.c b/drivers/gpu/drm/tilcdc/tilcdc_crtc.c
new file mode 100644
index 000000000..e07ecb36a
--- /dev/null
+++ b/drivers/gpu/drm/tilcdc/tilcdc_crtc.c
@@ -0,0 +1,1009 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2012 Texas Instruments
+ * Author: Rob Clark <robdclark@gmail.com>
+ */
+
+#include <linux/delay.h>
+#include <linux/dma-mapping.h>
+#include <linux/of_graph.h>
+#include <linux/pm_runtime.h>
+
+#include <drm/drm_atomic.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_crtc.h>
+#include <drm/drm_fb_dma_helper.h>
+#include <drm/drm_fourcc.h>
+#include <drm/drm_framebuffer.h>
+#include <drm/drm_gem_dma_helper.h>
+#include <drm/drm_managed.h>
+#include <drm/drm_modeset_helper_vtables.h>
+#include <drm/drm_print.h>
+#include <drm/drm_vblank.h>
+
+#include "tilcdc_drv.h"
+#include "tilcdc_regs.h"
+
+#define TILCDC_VBLANK_SAFETY_THRESHOLD_US 1000
+#define TILCDC_PALETTE_SIZE 32
+#define TILCDC_PALETTE_FIRST_ENTRY 0x4000
+
+struct tilcdc_crtc {
+ struct drm_crtc base;
+
+ struct tilcdc_plane *primary;
+ struct drm_pending_vblank_event *event;
+ struct mutex enable_lock;
+ bool enabled;
+ bool shutdown;
+ wait_queue_head_t frame_done_wq;
+ bool frame_done;
+ spinlock_t irq_lock;
+
+ unsigned int lcd_fck_rate;
+
+ ktime_t last_vblank;
+ unsigned int hvtotal_us;
+
+ struct drm_framebuffer *next_fb;
+
+ int sync_lost_count;
+ bool frame_intact;
+ struct work_struct recover_work;
+
+ dma_addr_t palette_dma_handle;
+ u16 *palette_base;
+ struct completion palette_loaded;
+};
+#define to_tilcdc_crtc(x) container_of(x, struct tilcdc_crtc, base)
+
+static void set_scanout(struct drm_crtc *crtc, struct drm_framebuffer *fb)
+{
+ struct drm_device *dev = crtc->dev;
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+ struct drm_gem_dma_object *gem;
+ dma_addr_t start, end;
+ u64 dma_base_and_ceiling;
+
+ gem = drm_fb_dma_get_gem_obj(fb, 0);
+
+ start = gem->dma_addr + fb->offsets[0] +
+ crtc->y * fb->pitches[0] +
+ crtc->x * fb->format->cpp[0];
+
+ end = start + (crtc->mode.vdisplay * fb->pitches[0]);
+
+ /* Write LCDC_DMA_FB_BASE_ADDR_0_REG and LCDC_DMA_FB_CEILING_ADDR_0_REG
+ * with a single insruction, if available. This should make it more
+ * unlikely that LCDC would fetch the DMA addresses in the middle of
+ * an update.
+ */
+ if (priv->rev == 1)
+ end -= 1;
+
+ dma_base_and_ceiling = (u64)end << 32 | start;
+ tilcdc_write64(dev, LCDC_DMA_FB_BASE_ADDR_0_REG, dma_base_and_ceiling);
+}
+
+/*
+ * The driver currently only supports only true color formats. For
+ * true color the palette block is bypassed, but a 32 byte palette
+ * should still be loaded. The first 16-bit entry must be 0x4000 while
+ * all other entries must be zeroed.
+ */
+static void tilcdc_crtc_load_palette(struct drm_crtc *crtc)
+{
+ struct tilcdc_crtc *tilcdc_crtc = to_tilcdc_crtc(crtc);
+ struct drm_device *dev = crtc->dev;
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+ int ret;
+
+ reinit_completion(&tilcdc_crtc->palette_loaded);
+
+ /* Tell the LCDC where the palette is located. */
+ tilcdc_write(dev, LCDC_DMA_FB_BASE_ADDR_0_REG,
+ tilcdc_crtc->palette_dma_handle);
+ tilcdc_write(dev, LCDC_DMA_FB_CEILING_ADDR_0_REG,
+ (u32) tilcdc_crtc->palette_dma_handle +
+ TILCDC_PALETTE_SIZE - 1);
+
+ /* Set dma load mode for palette loading only. */
+ tilcdc_write_mask(dev, LCDC_RASTER_CTRL_REG,
+ LCDC_PALETTE_LOAD_MODE(PALETTE_ONLY),
+ LCDC_PALETTE_LOAD_MODE_MASK);
+
+ /* Enable DMA Palette Loaded Interrupt */
+ if (priv->rev == 1)
+ tilcdc_set(dev, LCDC_RASTER_CTRL_REG, LCDC_V1_PL_INT_ENA);
+ else
+ tilcdc_write(dev, LCDC_INT_ENABLE_SET_REG, LCDC_V2_PL_INT_ENA);
+
+ /* Enable LCDC DMA and wait for palette to be loaded. */
+ tilcdc_clear_irqstatus(dev, 0xffffffff);
+ tilcdc_set(dev, LCDC_RASTER_CTRL_REG, LCDC_RASTER_ENABLE);
+
+ ret = wait_for_completion_timeout(&tilcdc_crtc->palette_loaded,
+ msecs_to_jiffies(50));
+ if (ret == 0)
+ drm_err(dev, "%s: Palette loading timeout", __func__);
+
+ /* Disable LCDC DMA and DMA Palette Loaded Interrupt. */
+ tilcdc_clear(dev, LCDC_RASTER_CTRL_REG, LCDC_RASTER_ENABLE);
+ if (priv->rev == 1)
+ tilcdc_clear(dev, LCDC_RASTER_CTRL_REG, LCDC_V1_PL_INT_ENA);
+ else
+ tilcdc_write(dev, LCDC_INT_ENABLE_CLR_REG, LCDC_V2_PL_INT_ENA);
+}
+
+static void tilcdc_crtc_enable_irqs(struct drm_device *dev)
+{
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+
+ tilcdc_clear_irqstatus(dev, 0xffffffff);
+
+ if (priv->rev == 1) {
+ tilcdc_set(dev, LCDC_RASTER_CTRL_REG,
+ LCDC_V1_SYNC_LOST_INT_ENA | LCDC_V1_FRAME_DONE_INT_ENA |
+ LCDC_V1_UNDERFLOW_INT_ENA);
+ } else {
+ tilcdc_write(dev, LCDC_INT_ENABLE_SET_REG,
+ LCDC_V2_UNDERFLOW_INT_ENA |
+ LCDC_FRAME_DONE | LCDC_SYNC_LOST);
+ }
+}
+
+static void tilcdc_crtc_disable_irqs(struct drm_device *dev)
+{
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+
+ /* disable irqs that we might have enabled: */
+ if (priv->rev == 1) {
+ tilcdc_clear(dev, LCDC_RASTER_CTRL_REG,
+ LCDC_V1_SYNC_LOST_INT_ENA | LCDC_V1_FRAME_DONE_INT_ENA |
+ LCDC_V1_UNDERFLOW_INT_ENA | LCDC_V1_PL_INT_ENA);
+ tilcdc_clear(dev, LCDC_DMA_CTRL_REG,
+ LCDC_V1_END_OF_FRAME_INT_ENA);
+ } else {
+ tilcdc_write(dev, LCDC_INT_ENABLE_CLR_REG,
+ LCDC_V2_UNDERFLOW_INT_ENA | LCDC_V2_PL_INT_ENA |
+ LCDC_V2_END_OF_FRAME0_INT_ENA |
+ LCDC_FRAME_DONE | LCDC_SYNC_LOST);
+ }
+}
+
+static void reset(struct drm_crtc *crtc)
+{
+ struct drm_device *dev = crtc->dev;
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+
+ if (priv->rev != 2)
+ return;
+
+ tilcdc_set(dev, LCDC_CLK_RESET_REG, LCDC_CLK_MAIN_RESET);
+ usleep_range(250, 1000);
+ tilcdc_clear(dev, LCDC_CLK_RESET_REG, LCDC_CLK_MAIN_RESET);
+}
+
+/*
+ * Calculate the percentage difference between the requested pixel clock rate
+ * and the effective rate resulting from calculating the clock divider value.
+ */
+static unsigned int tilcdc_pclk_diff(unsigned long rate,
+ unsigned long real_rate)
+{
+ int r = rate / 100, rr = real_rate / 100;
+
+ return (unsigned int)(abs(((rr - r) * 100) / r));
+}
+
+static void tilcdc_crtc_set_clk(struct drm_crtc *crtc)
+{
+ struct drm_device *dev = crtc->dev;
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+ struct tilcdc_crtc *tilcdc_crtc = to_tilcdc_crtc(crtc);
+ unsigned long clk_rate, real_pclk_rate, pclk_rate;
+ unsigned int clkdiv;
+ int ret;
+
+ clkdiv = 2; /* first try using a standard divider of 2 */
+
+ /* mode.clock is in KHz, set_rate wants parameter in Hz */
+ pclk_rate = crtc->mode.clock * 1000;
+
+ ret = clk_set_rate(priv->clk, pclk_rate * clkdiv);
+ clk_rate = clk_get_rate(priv->clk);
+ real_pclk_rate = clk_rate / clkdiv;
+ if (ret < 0 || tilcdc_pclk_diff(pclk_rate, real_pclk_rate) > 5) {
+ /*
+ * If we fail to set the clock rate (some architectures don't
+ * use the common clock framework yet and may not implement
+ * all the clk API calls for every clock), try the next best
+ * thing: adjusting the clock divider, unless clk_get_rate()
+ * failed as well.
+ */
+ if (!clk_rate) {
+ /* Nothing more we can do. Just bail out. */
+ drm_err(dev,
+ "failed to set the pixel clock - unable to read current lcdc clock rate\n");
+ return;
+ }
+
+ clkdiv = DIV_ROUND_CLOSEST(clk_rate, pclk_rate);
+
+ /*
+ * Emit a warning if the real clock rate resulting from the
+ * calculated divider differs much from the requested rate.
+ *
+ * 5% is an arbitrary value - LCDs are usually quite tolerant
+ * about pixel clock rates.
+ */
+ real_pclk_rate = clk_rate / clkdiv;
+
+ if (tilcdc_pclk_diff(pclk_rate, real_pclk_rate) > 5) {
+ drm_warn(dev,
+ "effective pixel clock rate (%luHz) differs from the requested rate (%luHz)\n",
+ real_pclk_rate, pclk_rate);
+ }
+ }
+
+ tilcdc_crtc->lcd_fck_rate = clk_rate;
+
+ DBG("lcd_clk=%u, mode clock=%d, div=%u",
+ tilcdc_crtc->lcd_fck_rate, crtc->mode.clock, clkdiv);
+
+ /* Configure the LCD clock divisor. */
+ tilcdc_write(dev, LCDC_CTRL_REG, LCDC_CLK_DIVISOR(clkdiv) |
+ LCDC_RASTER_MODE);
+
+ if (priv->rev == 2)
+ tilcdc_set(dev, LCDC_CLK_ENABLE_REG,
+ LCDC_V2_DMA_CLK_EN | LCDC_V2_LIDD_CLK_EN |
+ LCDC_V2_CORE_CLK_EN);
+}
+
+static uint tilcdc_mode_hvtotal(const struct drm_display_mode *mode)
+{
+ return (uint) div_u64(1000llu * mode->htotal * mode->vtotal,
+ mode->clock);
+}
+
+static void tilcdc_crtc_set_mode(struct drm_crtc *crtc)
+{
+ struct tilcdc_crtc *tilcdc_crtc = to_tilcdc_crtc(crtc);
+ struct drm_device *dev = crtc->dev;
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+ uint32_t reg, hbp, hfp, hsw, vbp, vfp, vsw;
+ struct drm_display_mode *mode = &crtc->state->adjusted_mode;
+ struct drm_framebuffer *fb = crtc->primary->state->fb;
+
+ if (WARN_ON(!fb))
+ return;
+
+ /* Configure the Burst Size and fifo threshold of DMA: */
+ reg = tilcdc_read(dev, LCDC_DMA_CTRL_REG) & ~0x00000770;
+ /* Use 16 bit DMA burst size by default */
+ reg |= LCDC_DMA_BURST_SIZE(LCDC_DMA_BURST_16);
+
+ if (priv->fifo_th) {
+ int fifo_th_val = ilog2(priv->fifo_th) - 3;
+
+ reg |= (fifo_th_val << 8);
+ }
+ tilcdc_write(dev, LCDC_DMA_CTRL_REG, reg);
+
+ /* Configure timings: */
+ hbp = mode->htotal - mode->hsync_end;
+ hfp = mode->hsync_start - mode->hdisplay;
+ hsw = mode->hsync_end - mode->hsync_start;
+ vbp = mode->vtotal - mode->vsync_end;
+ vfp = mode->vsync_start - mode->vdisplay;
+ vsw = mode->vsync_end - mode->vsync_start;
+
+ DBG("%dx%d, hbp=%u, hfp=%u, hsw=%u, vbp=%u, vfp=%u, vsw=%u",
+ mode->hdisplay, mode->vdisplay, hbp, hfp, hsw, vbp, vfp, vsw);
+
+ /* Set AC Bias Period and Number of Transitions per Interrupt: */
+ reg = tilcdc_read(dev, LCDC_RASTER_TIMING_2_REG) & ~0x000fff00;
+ /* Use 255 AC Bias Pin Frequency by default */
+ reg |= LCDC_AC_BIAS_FREQUENCY(255);
+
+ /*
+ * subtract one from hfp, hbp, hsw because the hardware uses
+ * a value of 0 as 1
+ */
+ if (priv->rev == 2) {
+ /* clear bits we're going to set */
+ reg &= ~0x78000033;
+ reg |= ((hfp-1) & 0x300) >> 8;
+ reg |= ((hbp-1) & 0x300) >> 4;
+ reg |= ((hsw-1) & 0x3c0) << 21;
+ }
+ tilcdc_write(dev, LCDC_RASTER_TIMING_2_REG, reg);
+
+ reg = (((mode->hdisplay >> 4) - 1) << 4) |
+ (((hbp-1) & 0xff) << 24) |
+ (((hfp-1) & 0xff) << 16) |
+ (((hsw-1) & 0x3f) << 10);
+ if (priv->rev == 2)
+ reg |= (((mode->hdisplay >> 4) - 1) & 0x40) >> 3;
+ tilcdc_write(dev, LCDC_RASTER_TIMING_0_REG, reg);
+
+ reg = ((mode->vdisplay - 1) & 0x3ff) |
+ ((vbp & 0xff) << 24) |
+ ((vfp & 0xff) << 16) |
+ (((vsw-1) & 0x3f) << 10);
+ tilcdc_write(dev, LCDC_RASTER_TIMING_1_REG, reg);
+
+ /*
+ * be sure to set Bit 10 for the V2 LCDC controller,
+ * otherwise limited to 1024 pixels width, stopping
+ * 1920x1080 being supported.
+ */
+ if (priv->rev == 2) {
+ if ((mode->vdisplay - 1) & 0x400) {
+ tilcdc_set(dev, LCDC_RASTER_TIMING_2_REG,
+ LCDC_LPP_B10);
+ } else {
+ tilcdc_clear(dev, LCDC_RASTER_TIMING_2_REG,
+ LCDC_LPP_B10);
+ }
+ }
+
+ /* Configure display type: */
+ reg = tilcdc_read(dev, LCDC_RASTER_CTRL_REG) &
+ ~(LCDC_TFT_MODE | LCDC_MONO_8BIT_MODE | LCDC_MONOCHROME_MODE |
+ LCDC_V2_TFT_24BPP_MODE | LCDC_V2_TFT_24BPP_UNPACK |
+ 0x000ff000 /* Palette Loading Delay bits */);
+ reg |= LCDC_TFT_MODE; /* no monochrome/passive support */
+ if (priv->rev == 2) {
+ switch (fb->format->format) {
+ case DRM_FORMAT_BGR565:
+ case DRM_FORMAT_RGB565:
+ break;
+ case DRM_FORMAT_XBGR8888:
+ case DRM_FORMAT_XRGB8888:
+ reg |= LCDC_V2_TFT_24BPP_UNPACK;
+ fallthrough;
+ case DRM_FORMAT_BGR888:
+ case DRM_FORMAT_RGB888:
+ reg |= LCDC_V2_TFT_24BPP_MODE;
+ break;
+ default:
+ drm_err(dev, "invalid pixel format\n");
+ return;
+ }
+ }
+ /* Use 128 FIFO DMA Request Delay by default */
+ reg |= 128 << 12;
+ tilcdc_write(dev, LCDC_RASTER_CTRL_REG, reg);
+
+ if (mode->flags == DRM_BUS_FLAG_PIXDATA_DRIVE_NEGEDGE)
+ tilcdc_set(dev, LCDC_RASTER_TIMING_2_REG, LCDC_INVERT_PIXEL_CLOCK);
+ else
+ tilcdc_clear(dev, LCDC_RASTER_TIMING_2_REG, LCDC_INVERT_PIXEL_CLOCK);
+
+ tilcdc_set(dev, LCDC_RASTER_TIMING_2_REG, LCDC_SYNC_CTRL);
+ if (mode->flags == DRM_BUS_FLAG_SYNC_DRIVE_NEGEDGE)
+ tilcdc_set(dev, LCDC_RASTER_TIMING_2_REG, LCDC_SYNC_EDGE);
+ else
+ tilcdc_clear(dev, LCDC_RASTER_TIMING_2_REG, LCDC_SYNC_EDGE);
+
+ if (mode->flags & DRM_MODE_FLAG_NHSYNC)
+ tilcdc_set(dev, LCDC_RASTER_TIMING_2_REG, LCDC_INVERT_HSYNC);
+ else
+ tilcdc_clear(dev, LCDC_RASTER_TIMING_2_REG, LCDC_INVERT_HSYNC);
+
+ if (mode->flags & DRM_MODE_FLAG_NVSYNC)
+ tilcdc_set(dev, LCDC_RASTER_TIMING_2_REG, LCDC_INVERT_VSYNC);
+ else
+ tilcdc_clear(dev, LCDC_RASTER_TIMING_2_REG, LCDC_INVERT_VSYNC);
+
+ tilcdc_crtc_set_clk(crtc);
+
+ tilcdc_crtc_load_palette(crtc);
+
+ set_scanout(crtc, fb);
+
+ drm_mode_copy(&crtc->hwmode, &crtc->state->adjusted_mode);
+
+ tilcdc_crtc->hvtotal_us =
+ tilcdc_mode_hvtotal(&crtc->hwmode);
+}
+
+static void tilcdc_crtc_enable(struct drm_crtc *crtc)
+{
+ struct drm_device *dev = crtc->dev;
+ struct tilcdc_crtc *tilcdc_crtc = to_tilcdc_crtc(crtc);
+ unsigned long flags;
+
+ mutex_lock(&tilcdc_crtc->enable_lock);
+ if (tilcdc_crtc->enabled || tilcdc_crtc->shutdown) {
+ mutex_unlock(&tilcdc_crtc->enable_lock);
+ return;
+ }
+
+ pm_runtime_get_sync(dev->dev);
+
+ reset(crtc);
+
+ tilcdc_crtc_set_mode(crtc);
+
+ tilcdc_crtc_enable_irqs(dev);
+
+ tilcdc_clear(dev, LCDC_DMA_CTRL_REG, LCDC_DUAL_FRAME_BUFFER_ENABLE);
+ tilcdc_write_mask(dev, LCDC_RASTER_CTRL_REG,
+ LCDC_PALETTE_LOAD_MODE(DATA_ONLY),
+ LCDC_PALETTE_LOAD_MODE_MASK);
+
+ /* There is no real chance for a race here as the time stamp
+ * is taken before the raster DMA is started. The spin-lock is
+ * taken to have a memory barrier after taking the time-stamp
+ * and to avoid a context switch between taking the stamp and
+ * enabling the raster.
+ */
+ spin_lock_irqsave(&tilcdc_crtc->irq_lock, flags);
+ tilcdc_crtc->last_vblank = ktime_get();
+ tilcdc_set(dev, LCDC_RASTER_CTRL_REG, LCDC_RASTER_ENABLE);
+ spin_unlock_irqrestore(&tilcdc_crtc->irq_lock, flags);
+
+ drm_crtc_vblank_on(crtc);
+
+ tilcdc_crtc->enabled = true;
+ mutex_unlock(&tilcdc_crtc->enable_lock);
+}
+
+static void tilcdc_crtc_atomic_enable(struct drm_crtc *crtc,
+ struct drm_atomic_commit *state)
+{
+ tilcdc_crtc_enable(crtc);
+}
+
+static void tilcdc_crtc_off(struct drm_crtc *crtc, bool shutdown)
+{
+ struct tilcdc_crtc *tilcdc_crtc = to_tilcdc_crtc(crtc);
+ struct drm_device *dev = crtc->dev;
+ int ret;
+
+ mutex_lock(&tilcdc_crtc->enable_lock);
+ if (shutdown)
+ tilcdc_crtc->shutdown = true;
+ if (!tilcdc_crtc->enabled) {
+ mutex_unlock(&tilcdc_crtc->enable_lock);
+ return;
+ }
+ tilcdc_crtc->frame_done = false;
+ tilcdc_clear(dev, LCDC_RASTER_CTRL_REG, LCDC_RASTER_ENABLE);
+
+ /*
+ * Wait for framedone irq which will still come before putting
+ * things to sleep..
+ */
+ ret = wait_event_timeout(tilcdc_crtc->frame_done_wq,
+ tilcdc_crtc->frame_done,
+ msecs_to_jiffies(500));
+ if (ret == 0)
+ drm_err(dev, "%s: timeout waiting for framedone\n",
+ __func__);
+
+ drm_crtc_vblank_off(crtc);
+
+ spin_lock_irq(&crtc->dev->event_lock);
+
+ if (crtc->state->event) {
+ drm_crtc_send_vblank_event(crtc, crtc->state->event);
+ crtc->state->event = NULL;
+ }
+
+ spin_unlock_irq(&crtc->dev->event_lock);
+
+ tilcdc_crtc_disable_irqs(dev);
+
+ pm_runtime_put_sync(dev->dev);
+
+ tilcdc_crtc->enabled = false;
+ mutex_unlock(&tilcdc_crtc->enable_lock);
+}
+
+static void tilcdc_crtc_disable(struct drm_crtc *crtc)
+{
+ tilcdc_crtc_off(crtc, false);
+}
+
+static void tilcdc_crtc_atomic_disable(struct drm_crtc *crtc,
+ struct drm_atomic_commit *state)
+{
+ tilcdc_crtc_disable(crtc);
+}
+
+static void tilcdc_crtc_atomic_flush(struct drm_crtc *crtc,
+ struct drm_atomic_commit *state)
+{
+ if (!crtc->state->event)
+ return;
+
+ spin_lock_irq(&crtc->dev->event_lock);
+ drm_crtc_send_vblank_event(crtc, crtc->state->event);
+ crtc->state->event = NULL;
+ spin_unlock_irq(&crtc->dev->event_lock);
+}
+
+void tilcdc_crtc_shutdown(struct drm_crtc *crtc)
+{
+ tilcdc_crtc_off(crtc, true);
+}
+
+static bool tilcdc_crtc_is_on(struct drm_crtc *crtc)
+{
+ return crtc->state && crtc->state->enable && crtc->state->active;
+}
+
+static void tilcdc_crtc_recover_work(struct work_struct *work)
+{
+ struct tilcdc_crtc *tilcdc_crtc =
+ container_of(work, struct tilcdc_crtc, recover_work);
+ struct drm_crtc *crtc = &tilcdc_crtc->base;
+
+ drm_info(crtc->dev, "%s: Reset CRTC", __func__);
+
+ drm_modeset_lock(&crtc->mutex, NULL);
+
+ if (!tilcdc_crtc_is_on(crtc))
+ goto out;
+
+ tilcdc_crtc_disable(crtc);
+ tilcdc_crtc_enable(crtc);
+out:
+ drm_modeset_unlock(&crtc->mutex);
+}
+
+static void tilcdc_crtc_destroy(struct drm_device *dev, void *data)
+{
+ struct tilcdc_drm_private *priv = (struct tilcdc_drm_private *)data;
+
+ tilcdc_crtc_shutdown(priv->crtc);
+
+ flush_workqueue(priv->wq);
+
+ of_node_put(priv->crtc->port);
+}
+
+int tilcdc_crtc_update_fb(struct drm_crtc *crtc,
+ struct drm_framebuffer *fb,
+ struct drm_pending_vblank_event *event)
+{
+ struct tilcdc_crtc *tilcdc_crtc = to_tilcdc_crtc(crtc);
+ struct drm_device *dev = crtc->dev;
+
+ if (tilcdc_crtc->event) {
+ drm_err(dev, "already pending page flip!\n");
+ return -EBUSY;
+ }
+
+ tilcdc_crtc->event = event;
+
+ mutex_lock(&tilcdc_crtc->enable_lock);
+
+ if (tilcdc_crtc->enabled) {
+ unsigned long flags;
+ ktime_t next_vblank;
+ s64 tdiff;
+
+ spin_lock_irqsave(&tilcdc_crtc->irq_lock, flags);
+
+ next_vblank = ktime_add_us(tilcdc_crtc->last_vblank,
+ tilcdc_crtc->hvtotal_us);
+ tdiff = ktime_to_us(ktime_sub(next_vblank, ktime_get()));
+
+ if (tdiff < TILCDC_VBLANK_SAFETY_THRESHOLD_US)
+ tilcdc_crtc->next_fb = fb;
+ else
+ set_scanout(crtc, fb);
+
+ spin_unlock_irqrestore(&tilcdc_crtc->irq_lock, flags);
+ }
+
+ mutex_unlock(&tilcdc_crtc->enable_lock);
+
+ return 0;
+}
+
+static bool tilcdc_crtc_mode_fixup(struct drm_crtc *crtc,
+ const struct drm_display_mode *mode,
+ struct drm_display_mode *adjusted_mode)
+{
+ /*
+ * tilcdc does not generate VESA-compliant sync but aligns
+ * VS on the second edge of HS instead of first edge.
+ * We use adjusted_mode, to fixup sync by aligning both rising
+ * edges and add HSKEW offset to fix the sync.
+ */
+ adjusted_mode->hskew = mode->hsync_end - mode->hsync_start;
+ adjusted_mode->flags |= DRM_MODE_FLAG_HSKEW;
+
+ if (mode->flags & DRM_MODE_FLAG_NHSYNC) {
+ adjusted_mode->flags |= DRM_MODE_FLAG_PHSYNC;
+ adjusted_mode->flags &= ~DRM_MODE_FLAG_NHSYNC;
+ } else {
+ adjusted_mode->flags |= DRM_MODE_FLAG_NHSYNC;
+ adjusted_mode->flags &= ~DRM_MODE_FLAG_PHSYNC;
+ }
+
+ return true;
+}
+
+static int tilcdc_crtc_atomic_check(struct drm_crtc *crtc,
+ struct drm_atomic_commit *state)
+{
+ struct drm_crtc_state *crtc_state = drm_atomic_get_new_crtc_state(state,
+ crtc);
+ /* If we are not active we don't care */
+ if (!crtc_state->active)
+ return 0;
+
+ return drm_atomic_helper_check_crtc_primary_plane(crtc_state);
+}
+
+static int tilcdc_crtc_enable_vblank(struct drm_crtc *crtc)
+{
+ struct tilcdc_crtc *tilcdc_crtc = to_tilcdc_crtc(crtc);
+ struct drm_device *dev = crtc->dev;
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+ unsigned long flags;
+
+ spin_lock_irqsave(&tilcdc_crtc->irq_lock, flags);
+
+ tilcdc_clear_irqstatus(dev, LCDC_END_OF_FRAME0);
+
+ if (priv->rev == 1)
+ tilcdc_set(dev, LCDC_DMA_CTRL_REG,
+ LCDC_V1_END_OF_FRAME_INT_ENA);
+ else
+ tilcdc_set(dev, LCDC_INT_ENABLE_SET_REG,
+ LCDC_V2_END_OF_FRAME0_INT_ENA);
+
+ spin_unlock_irqrestore(&tilcdc_crtc->irq_lock, flags);
+
+ return 0;
+}
+
+static void tilcdc_crtc_disable_vblank(struct drm_crtc *crtc)
+{
+ struct tilcdc_crtc *tilcdc_crtc = to_tilcdc_crtc(crtc);
+ struct drm_device *dev = crtc->dev;
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+ unsigned long flags;
+
+ spin_lock_irqsave(&tilcdc_crtc->irq_lock, flags);
+
+ if (priv->rev == 1)
+ tilcdc_clear(dev, LCDC_DMA_CTRL_REG,
+ LCDC_V1_END_OF_FRAME_INT_ENA);
+ else
+ tilcdc_clear(dev, LCDC_INT_ENABLE_SET_REG,
+ LCDC_V2_END_OF_FRAME0_INT_ENA);
+
+ spin_unlock_irqrestore(&tilcdc_crtc->irq_lock, flags);
+}
+
+static void tilcdc_crtc_reset(struct drm_crtc *crtc)
+{
+ struct tilcdc_crtc *tilcdc_crtc = to_tilcdc_crtc(crtc);
+ struct drm_device *dev = crtc->dev;
+ int ret;
+
+ drm_atomic_helper_crtc_reset(crtc);
+
+ /* Turn the raster off if it for some reason is on. */
+ pm_runtime_get_sync(dev->dev);
+ if (tilcdc_read(dev, LCDC_RASTER_CTRL_REG) & LCDC_RASTER_ENABLE) {
+ /* Enable DMA Frame Done Interrupt */
+ tilcdc_write(dev, LCDC_INT_ENABLE_SET_REG, LCDC_FRAME_DONE);
+ tilcdc_clear_irqstatus(dev, 0xffffffff);
+
+ tilcdc_crtc->frame_done = false;
+ tilcdc_clear(dev, LCDC_RASTER_CTRL_REG, LCDC_RASTER_ENABLE);
+
+ ret = wait_event_timeout(tilcdc_crtc->frame_done_wq,
+ tilcdc_crtc->frame_done,
+ msecs_to_jiffies(500));
+ if (ret == 0)
+ drm_err(dev, "%s: timeout waiting for framedone\n",
+ __func__);
+ }
+ pm_runtime_put_sync(dev->dev);
+}
+
+static const struct drm_crtc_funcs tilcdc_crtc_funcs = {
+ .set_config = drm_atomic_helper_set_config,
+ .page_flip = drm_atomic_helper_page_flip,
+ .reset = tilcdc_crtc_reset,
+ .atomic_duplicate_state = drm_atomic_helper_crtc_duplicate_state,
+ .atomic_destroy_state = drm_atomic_helper_crtc_destroy_state,
+ .enable_vblank = tilcdc_crtc_enable_vblank,
+ .disable_vblank = tilcdc_crtc_disable_vblank,
+};
+
+static enum drm_mode_status
+tilcdc_crtc_mode_valid(struct drm_crtc *crtc,
+ const struct drm_display_mode *mode)
+{
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(crtc->dev);
+ unsigned int bandwidth;
+ uint32_t hbp, hfp, hsw, vbp, vfp, vsw;
+
+ /*
+ * check to see if the width is within the range that
+ * the LCD Controller physically supports
+ */
+ if (mode->hdisplay > priv->max_width)
+ return MODE_VIRTUAL_X;
+
+ /* width must be multiple of 16 */
+ if (mode->hdisplay & 0xf)
+ return MODE_VIRTUAL_X;
+
+ if (mode->vdisplay > 2048)
+ return MODE_VIRTUAL_Y;
+
+ DBG("Processing mode %dx%d@%d with pixel clock %d",
+ mode->hdisplay, mode->vdisplay,
+ drm_mode_vrefresh(mode), mode->clock);
+
+ hbp = mode->htotal - mode->hsync_end;
+ hfp = mode->hsync_start - mode->hdisplay;
+ hsw = mode->hsync_end - mode->hsync_start;
+ vbp = mode->vtotal - mode->vsync_end;
+ vfp = mode->vsync_start - mode->vdisplay;
+ vsw = mode->vsync_end - mode->vsync_start;
+
+ if ((hbp-1) & ~0x3ff) {
+ DBG("Pruning mode: Horizontal Back Porch out of range");
+ return MODE_HBLANK_WIDE;
+ }
+
+ if ((hfp-1) & ~0x3ff) {
+ DBG("Pruning mode: Horizontal Front Porch out of range");
+ return MODE_HBLANK_WIDE;
+ }
+
+ if ((hsw-1) & ~0x3ff) {
+ DBG("Pruning mode: Horizontal Sync Width out of range");
+ return MODE_HSYNC_WIDE;
+ }
+
+ if (vbp & ~0xff) {
+ DBG("Pruning mode: Vertical Back Porch out of range");
+ return MODE_VBLANK_WIDE;
+ }
+
+ if (vfp & ~0xff) {
+ DBG("Pruning mode: Vertical Front Porch out of range");
+ return MODE_VBLANK_WIDE;
+ }
+
+ if ((vsw-1) & ~0x3f) {
+ DBG("Pruning mode: Vertical Sync Width out of range");
+ return MODE_VSYNC_WIDE;
+ }
+
+ /*
+ * some devices have a maximum allowed pixel clock
+ * configured from the DT
+ */
+ if (mode->clock > priv->max_pixelclock) {
+ DBG("Pruning mode: pixel clock too high");
+ return MODE_CLOCK_HIGH;
+ }
+
+ /*
+ * some devices further limit the max horizontal resolution
+ * configured from the DT
+ */
+ if (mode->hdisplay > priv->max_width)
+ return MODE_BAD_WIDTH;
+
+ /* filter out modes that would require too much memory bandwidth: */
+ bandwidth = mode->hdisplay * mode->vdisplay *
+ drm_mode_vrefresh(mode);
+ if (bandwidth > priv->max_bandwidth) {
+ DBG("Pruning mode: exceeds defined bandwidth limit");
+ return MODE_BAD;
+ }
+
+ return MODE_OK;
+}
+
+static const struct drm_crtc_helper_funcs tilcdc_crtc_helper_funcs = {
+ .mode_valid = tilcdc_crtc_mode_valid,
+ .mode_fixup = tilcdc_crtc_mode_fixup,
+ .atomic_check = tilcdc_crtc_atomic_check,
+ .atomic_enable = tilcdc_crtc_atomic_enable,
+ .atomic_disable = tilcdc_crtc_atomic_disable,
+ .atomic_flush = tilcdc_crtc_atomic_flush,
+};
+
+void tilcdc_crtc_update_clk(struct drm_crtc *crtc)
+{
+ struct drm_device *dev = crtc->dev;
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+ struct tilcdc_crtc *tilcdc_crtc = to_tilcdc_crtc(crtc);
+
+ drm_modeset_lock(&crtc->mutex, NULL);
+ if (tilcdc_crtc->lcd_fck_rate != clk_get_rate(priv->clk)) {
+ if (tilcdc_crtc_is_on(crtc)) {
+ pm_runtime_get_sync(dev->dev);
+ tilcdc_crtc_disable(crtc);
+
+ tilcdc_crtc_set_clk(crtc);
+
+ tilcdc_crtc_enable(crtc);
+ pm_runtime_put_sync(dev->dev);
+ }
+ }
+ drm_modeset_unlock(&crtc->mutex);
+}
+
+#define SYNC_LOST_COUNT_LIMIT 50
+
+irqreturn_t tilcdc_crtc_irq(struct drm_crtc *crtc)
+{
+ struct tilcdc_crtc *tilcdc_crtc = to_tilcdc_crtc(crtc);
+ struct drm_device *dev = crtc->dev;
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+ uint32_t stat, reg;
+
+ stat = tilcdc_read_irqstatus(dev);
+ tilcdc_clear_irqstatus(dev, stat);
+
+ if (stat & LCDC_END_OF_FRAME0) {
+ bool skip_event = false;
+ ktime_t now;
+
+ now = ktime_get();
+
+ spin_lock(&tilcdc_crtc->irq_lock);
+
+ tilcdc_crtc->last_vblank = now;
+
+ if (tilcdc_crtc->next_fb) {
+ set_scanout(crtc, tilcdc_crtc->next_fb);
+ tilcdc_crtc->next_fb = NULL;
+ skip_event = true;
+ }
+
+ spin_unlock(&tilcdc_crtc->irq_lock);
+
+ drm_crtc_handle_vblank(crtc);
+
+ if (!skip_event) {
+ struct drm_pending_vblank_event *event;
+
+ spin_lock(&dev->event_lock);
+
+ event = tilcdc_crtc->event;
+ tilcdc_crtc->event = NULL;
+ if (event)
+ drm_crtc_send_vblank_event(crtc, event);
+
+ spin_unlock(&dev->event_lock);
+ }
+
+ if (tilcdc_crtc->frame_intact)
+ tilcdc_crtc->sync_lost_count = 0;
+ else
+ tilcdc_crtc->frame_intact = true;
+ }
+
+ if (stat & LCDC_FIFO_UNDERFLOW)
+ drm_err_ratelimited(dev, "%s(0x%08x): FIFO underflow",
+ __func__, stat);
+
+ if (stat & LCDC_PL_LOAD_DONE) {
+ complete(&tilcdc_crtc->palette_loaded);
+ if (priv->rev == 1)
+ tilcdc_clear(dev, LCDC_RASTER_CTRL_REG,
+ LCDC_V1_PL_INT_ENA);
+ else
+ tilcdc_write(dev, LCDC_INT_ENABLE_CLR_REG,
+ LCDC_V2_PL_INT_ENA);
+ }
+
+ if (stat & LCDC_SYNC_LOST) {
+ drm_err_ratelimited(dev, "%s(0x%08x): Sync lost",
+ __func__, stat);
+ tilcdc_crtc->frame_intact = false;
+ if (priv->rev == 1) {
+ reg = tilcdc_read(dev, LCDC_RASTER_CTRL_REG);
+ if (reg & LCDC_RASTER_ENABLE) {
+ tilcdc_clear(dev, LCDC_RASTER_CTRL_REG,
+ LCDC_RASTER_ENABLE);
+ tilcdc_set(dev, LCDC_RASTER_CTRL_REG,
+ LCDC_RASTER_ENABLE);
+ }
+ } else {
+ if (tilcdc_crtc->sync_lost_count++ >
+ SYNC_LOST_COUNT_LIMIT) {
+ drm_err(dev,
+ "%s(0x%08x): Sync lost flood detected, recovering",
+ __func__, stat);
+ queue_work(system_percpu_wq,
+ &tilcdc_crtc->recover_work);
+ tilcdc_write(dev, LCDC_INT_ENABLE_CLR_REG,
+ LCDC_SYNC_LOST);
+ tilcdc_crtc->sync_lost_count = 0;
+ }
+ }
+ }
+
+ if (stat & LCDC_FRAME_DONE) {
+ tilcdc_crtc->frame_done = true;
+ wake_up(&tilcdc_crtc->frame_done_wq);
+ /* rev 1 lcdc appears to hang if irq is not disabled here */
+ if (priv->rev == 1)
+ tilcdc_clear(dev, LCDC_RASTER_CTRL_REG,
+ LCDC_V1_FRAME_DONE_INT_ENA);
+ }
+
+ /* For revision 2 only */
+ if (priv->rev == 2) {
+ /* Indicate to LCDC that the interrupt service routine has
+ * completed, see 13.3.6.1.6 in AM335x TRM.
+ */
+ tilcdc_write(dev, LCDC_END_OF_INT_IND_REG, 0);
+ }
+
+ return IRQ_HANDLED;
+}
+
+int tilcdc_crtc_create(struct drm_device *dev)
+{
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+ struct tilcdc_crtc *tilcdc_crtc;
+ struct tilcdc_plane *primary;
+ struct drm_crtc *crtc;
+ int ret;
+
+ primary = tilcdc_plane_init(dev);
+ if (IS_ERR(primary)) {
+ drm_err(dev, "Failed to initialize plane: %pe\n", primary);
+ return PTR_ERR(primary);
+ }
+
+ tilcdc_crtc = drmm_crtc_alloc_with_planes(dev, struct tilcdc_crtc, base,
+ &primary->base,
+ NULL,
+ &tilcdc_crtc_funcs,
+ "tilcdc crtc");
+ if (IS_ERR(tilcdc_crtc)) {
+ drm_err(dev, "Failed to init CRTC: %pe\n", tilcdc_crtc);
+ return PTR_ERR(tilcdc_crtc);
+ }
+
+ tilcdc_crtc->primary = primary;
+ priv->crtc = &tilcdc_crtc->base;
+ ret = drmm_add_action_or_reset(dev, tilcdc_crtc_destroy, priv);
+ if (ret)
+ return ret;
+
+ init_completion(&tilcdc_crtc->palette_loaded);
+ tilcdc_crtc->palette_base = dmam_alloc_coherent(dev->dev,
+ TILCDC_PALETTE_SIZE,
+ &tilcdc_crtc->palette_dma_handle,
+ GFP_KERNEL | __GFP_ZERO);
+ if (!tilcdc_crtc->palette_base)
+ return -ENOMEM;
+ *tilcdc_crtc->palette_base = TILCDC_PALETTE_FIRST_ENTRY;
+
+ crtc = &tilcdc_crtc->base;
+
+ mutex_init(&tilcdc_crtc->enable_lock);
+
+ init_waitqueue_head(&tilcdc_crtc->frame_done_wq);
+
+ spin_lock_init(&tilcdc_crtc->irq_lock);
+ INIT_WORK(&tilcdc_crtc->recover_work, tilcdc_crtc_recover_work);
+
+ drm_crtc_helper_add(crtc, &tilcdc_crtc_helper_funcs);
+
+ return 0;
+}
diff --git a/drivers/gpu/drm/tilcdc/tilcdc_drv.c b/drivers/gpu/drm/tilcdc/tilcdc_drv.c
new file mode 100644
index 000000000..1d6c9a423
--- /dev/null
+++ b/drivers/gpu/drm/tilcdc/tilcdc_drv.c
@@ -0,0 +1,501 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2012 Texas Instruments
+ * Author: Rob Clark <robdclark@gmail.com>
+ */
+
+/* LCDC DRM driver, based on da8xx-fb */
+
+#include <linux/pinctrl/consumer.h>
+#include <linux/platform_device.h>
+#include <linux/pm_runtime.h>
+
+#include <drm/clients/drm_client_setup.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_debugfs.h>
+#include <drm/drm_drv.h>
+#include <drm/drm_fbdev_dma.h>
+#include <drm/drm_fourcc.h>
+#include <drm/drm_gem_dma_helper.h>
+#include <drm/drm_gem_framebuffer_helper.h>
+#include <drm/drm_mm.h>
+#include <drm/drm_module.h>
+#include <drm/drm_probe_helper.h>
+#include <drm/drm_vblank.h>
+
+
+#include "tilcdc_drv.h"
+#include "tilcdc_encoder.h"
+#include "tilcdc_regs.h"
+
+enum tilcdc_variant {
+ AM33XX_TILCDC,
+ DA850_TILCDC,
+};
+
+static const u32 tilcdc_rev1_formats[] = { DRM_FORMAT_RGB565 };
+
+static const u32 tilcdc_straight_formats[] = { DRM_FORMAT_RGB565,
+ DRM_FORMAT_BGR888,
+ DRM_FORMAT_XBGR8888 };
+
+static const u32 tilcdc_crossed_formats[] = { DRM_FORMAT_BGR565,
+ DRM_FORMAT_RGB888,
+ DRM_FORMAT_XRGB8888 };
+
+static const u32 tilcdc_legacy_formats[] = { DRM_FORMAT_RGB565,
+ DRM_FORMAT_RGB888,
+ DRM_FORMAT_XRGB8888 };
+
+static int tilcdc_atomic_check(struct drm_device *dev,
+ struct drm_atomic_commit *state)
+{
+ int ret;
+
+ ret = drm_atomic_helper_check_modeset(dev, state);
+ if (ret)
+ return ret;
+
+ ret = drm_atomic_helper_check_planes(dev, state);
+ if (ret)
+ return ret;
+
+ /*
+ * tilcdc ->atomic_check can update ->mode_changed if pixel format
+ * changes, hence will we check modeset changes again.
+ */
+ ret = drm_atomic_helper_check_modeset(dev, state);
+ if (ret)
+ return ret;
+
+ return ret;
+}
+
+static const struct drm_mode_config_funcs mode_config_funcs = {
+ .fb_create = drm_gem_fb_create,
+ .atomic_check = tilcdc_atomic_check,
+ .atomic_commit = drm_atomic_helper_commit,
+};
+
+static void modeset_init(struct drm_device *dev)
+{
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+
+ dev->mode_config.min_width = 0;
+ dev->mode_config.min_height = 0;
+ dev->mode_config.max_width = priv->max_width;
+ dev->mode_config.max_height = 2048;
+ dev->mode_config.funcs = &mode_config_funcs;
+}
+
+#ifdef CONFIG_CPU_FREQ
+static int cpufreq_transition(struct notifier_block *nb,
+ unsigned long val, void *data)
+{
+ struct tilcdc_drm_private *priv = container_of(nb,
+ struct tilcdc_drm_private, freq_transition);
+
+ if (val == CPUFREQ_POSTCHANGE)
+ tilcdc_crtc_update_clk(priv->crtc);
+
+ return 0;
+}
+#endif
+
+static irqreturn_t tilcdc_irq(int irq, void *arg)
+{
+ struct drm_device *dev = arg;
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+
+ return tilcdc_crtc_irq(priv->crtc);
+}
+
+static int tilcdc_irq_install(struct drm_device *dev, unsigned int irq)
+{
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+ int ret;
+
+ ret = request_irq(irq, tilcdc_irq, 0, dev->driver->name, dev);
+ if (ret)
+ return ret;
+
+ priv->irq_enabled = true;
+
+ return 0;
+}
+
+static void tilcdc_irq_uninstall(struct drm_device *dev)
+{
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+
+ if (!priv->irq_enabled)
+ return;
+
+ free_irq(priv->irq, dev);
+ priv->irq_enabled = false;
+}
+
+/*
+ * DRM operations:
+ */
+
+#if defined(CONFIG_DEBUG_FS)
+static const struct {
+ const char *name;
+ uint8_t rev;
+ uint8_t save;
+ uint32_t reg;
+} registers[] = {
+#define REG(rev, save, reg) { #reg, rev, save, reg }
+ /* exists in revision 1: */
+ REG(1, false, LCDC_PID_REG),
+ REG(1, true, LCDC_CTRL_REG),
+ REG(1, false, LCDC_STAT_REG),
+ REG(1, true, LCDC_RASTER_CTRL_REG),
+ REG(1, true, LCDC_RASTER_TIMING_0_REG),
+ REG(1, true, LCDC_RASTER_TIMING_1_REG),
+ REG(1, true, LCDC_RASTER_TIMING_2_REG),
+ REG(1, true, LCDC_DMA_CTRL_REG),
+ REG(1, true, LCDC_DMA_FB_BASE_ADDR_0_REG),
+ REG(1, true, LCDC_DMA_FB_CEILING_ADDR_0_REG),
+ REG(1, true, LCDC_DMA_FB_BASE_ADDR_1_REG),
+ REG(1, true, LCDC_DMA_FB_CEILING_ADDR_1_REG),
+ /* new in revision 2: */
+ REG(2, false, LCDC_RAW_STAT_REG),
+ REG(2, false, LCDC_MASKED_STAT_REG),
+ REG(2, true, LCDC_INT_ENABLE_SET_REG),
+ REG(2, false, LCDC_INT_ENABLE_CLR_REG),
+ REG(2, false, LCDC_END_OF_INT_IND_REG),
+ REG(2, true, LCDC_CLK_ENABLE_REG),
+#undef REG
+};
+
+static int tilcdc_regs_show(struct seq_file *m, void *arg)
+{
+ struct drm_info_node *node = (struct drm_info_node *) m->private;
+ struct drm_device *dev = node->minor->dev;
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+ unsigned i;
+
+ pm_runtime_get_sync(dev->dev);
+
+ seq_printf(m, "revision: %d\n", priv->rev);
+
+ for (i = 0; i < ARRAY_SIZE(registers); i++)
+ if (priv->rev >= registers[i].rev)
+ seq_printf(m, "%s:\t %08x\n", registers[i].name,
+ tilcdc_read(dev, registers[i].reg));
+
+ pm_runtime_put_sync(dev->dev);
+
+ return 0;
+}
+
+static int tilcdc_mm_show(struct seq_file *m, void *arg)
+{
+ struct drm_info_node *node = (struct drm_info_node *) m->private;
+ struct drm_device *dev = node->minor->dev;
+ struct drm_printer p = drm_seq_file_printer(m);
+ drm_mm_print(&dev->vma_offset_manager->vm_addr_space_mm, &p);
+ return 0;
+}
+
+static struct drm_info_list tilcdc_debugfs_list[] = {
+ { "regs", tilcdc_regs_show, 0, NULL },
+ { "mm", tilcdc_mm_show, 0, NULL },
+};
+
+static void tilcdc_debugfs_init(struct drm_minor *minor)
+{
+ drm_debugfs_create_files(tilcdc_debugfs_list,
+ ARRAY_SIZE(tilcdc_debugfs_list),
+ minor->debugfs_root, minor);
+}
+#endif
+
+DEFINE_DRM_GEM_DMA_FOPS(fops);
+
+static const struct drm_driver tilcdc_driver = {
+ .driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_ATOMIC,
+ DRM_GEM_DMA_DRIVER_OPS,
+ DRM_FBDEV_DMA_DRIVER_OPS,
+#ifdef CONFIG_DEBUG_FS
+ .debugfs_init = tilcdc_debugfs_init,
+#endif
+ .fops = &fops,
+ .name = "tilcdc",
+ .desc = "TI LCD Controller DRM",
+ .major = 1,
+ .minor = 0,
+};
+
+/*
+ * Power management:
+ */
+
+static int tilcdc_pm_suspend(struct device *dev)
+{
+ struct drm_device *ddev = dev_get_drvdata(dev);
+ int ret = 0;
+
+ ret = drm_mode_config_helper_suspend(ddev);
+
+ /* Select sleep pin state */
+ pinctrl_pm_select_sleep_state(dev);
+
+ return ret;
+}
+
+static int tilcdc_pm_resume(struct device *dev)
+{
+ struct drm_device *ddev = dev_get_drvdata(dev);
+
+ /* Select default pin state */
+ pinctrl_pm_select_default_state(dev);
+ return drm_mode_config_helper_resume(ddev);
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(tilcdc_pm_ops,
+ tilcdc_pm_suspend, tilcdc_pm_resume);
+
+static int tilcdc_pdev_probe(struct platform_device *pdev)
+{
+ struct device_node *node = pdev->dev.of_node;
+ struct tilcdc_drm_private *priv;
+ struct device *dev = &pdev->dev;
+ enum tilcdc_variant variant;
+ struct drm_device *ddev;
+ u32 bpp = 0;
+ int ret;
+
+ priv = devm_drm_dev_alloc(dev, &tilcdc_driver,
+ struct tilcdc_drm_private, ddev);
+ if (IS_ERR(priv))
+ return PTR_ERR(priv);
+
+ variant = (uintptr_t)of_device_get_match_data(dev);
+
+ platform_set_drvdata(pdev, priv);
+ ddev = &priv->ddev;
+ ret = drmm_mode_config_init(ddev);
+ if (ret)
+ return ret;
+
+ priv->wq = alloc_ordered_workqueue("tilcdc", 0);
+ if (!priv->wq)
+ return -ENOMEM;
+
+ priv->mmio = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(priv->mmio)) {
+ drm_err(ddev, "failed to request / ioremap\n");
+ ret = PTR_ERR(priv->mmio);
+ goto free_wq;
+ }
+
+ priv->clk = clk_get(dev, "fck");
+ if (IS_ERR(priv->clk)) {
+ drm_err(ddev, "failed to get functional clock\n");
+ ret = -ENODEV;
+ goto free_wq;
+ }
+
+ pm_runtime_enable(dev);
+
+ /* Determine LCD IP Version */
+ pm_runtime_get_sync(dev);
+ switch (tilcdc_read(ddev, LCDC_PID_REG)) {
+ case 0x4c100102:
+ priv->rev = 1;
+ break;
+ case 0x4f200800:
+ case 0x4f201000:
+ priv->rev = 2;
+ break;
+ default:
+ drm_warn(ddev, "Unknown PID Reg value 0x%08x, "
+ "defaulting to LCD revision 1\n",
+ tilcdc_read(ddev, LCDC_PID_REG));
+ priv->rev = 1;
+ break;
+ }
+
+ pm_runtime_put_sync(dev);
+
+ if (priv->rev == 1) {
+ DBG("Revision 1 LCDC supports only RGB565 format");
+ priv->pixelformats = tilcdc_rev1_formats;
+ priv->num_pixelformats = ARRAY_SIZE(tilcdc_rev1_formats);
+ bpp = 16;
+ } else {
+ const char *str = "\0";
+
+ of_property_read_string(node, "blue-and-red-wiring", &str);
+ if (0 == strcmp(str, "crossed")) {
+ DBG("Configured for crossed blue and red wires");
+ priv->pixelformats = tilcdc_crossed_formats;
+ priv->num_pixelformats =
+ ARRAY_SIZE(tilcdc_crossed_formats);
+ bpp = 32; /* Choose bpp with RGB support for fbdef */
+ } else if (0 == strcmp(str, "straight")) {
+ DBG("Configured for straight blue and red wires");
+ priv->pixelformats = tilcdc_straight_formats;
+ priv->num_pixelformats =
+ ARRAY_SIZE(tilcdc_straight_formats);
+ bpp = 16; /* Choose bpp with RGB support for fbdef */
+ } else {
+ DBG("Blue and red wiring '%s' unknown, use legacy mode",
+ str);
+ priv->pixelformats = tilcdc_legacy_formats;
+ priv->num_pixelformats =
+ ARRAY_SIZE(tilcdc_legacy_formats);
+ bpp = 16; /* This is just a guess */
+ }
+ }
+
+ if (of_property_read_u32(node, "max-bandwidth", &priv->max_bandwidth))
+ priv->max_bandwidth = TILCDC_DEFAULT_MAX_BANDWIDTH;
+
+ DBG("Maximum Bandwidth Value %d", priv->max_bandwidth);
+
+ if (of_property_read_u32(node, "max-width", &priv->max_width)) {
+ if (priv->rev == 1)
+ priv->max_width = TILCDC_DEFAULT_MAX_WIDTH_V1;
+ else
+ priv->max_width = TILCDC_DEFAULT_MAX_WIDTH_V2;
+ }
+
+ DBG("Maximum Horizontal Pixel Width Value %dpixels", priv->max_width);
+
+ if (of_property_read_u32(node, "max-pixelclock",
+ &priv->max_pixelclock))
+ priv->max_pixelclock = TILCDC_DEFAULT_MAX_PIXELCLOCK;
+
+ DBG("Maximum Pixel Clock Value %dKHz", priv->max_pixelclock);
+
+ if (variant == DA850_TILCDC)
+ priv->fifo_th = 16;
+ else
+ priv->fifo_th = 8;
+
+ ret = tilcdc_crtc_create(ddev);
+ if (ret < 0) {
+ drm_err(ddev, "failed to create crtc\n");
+ goto disable_pm;
+ }
+ modeset_init(ddev);
+
+#ifdef CONFIG_CPU_FREQ
+ priv->freq_transition.notifier_call = cpufreq_transition;
+ ret = cpufreq_register_notifier(&priv->freq_transition,
+ CPUFREQ_TRANSITION_NOTIFIER);
+ if (ret) {
+ drm_err(ddev, "failed to register cpufreq notifier\n");
+ priv->freq_transition.notifier_call = NULL;
+ goto disable_pm;
+ }
+#endif
+
+ ret = tilcdc_encoder_create(ddev);
+ if (ret)
+ goto unregister_cpufreq_notif;
+
+ if (!priv->connector) {
+ drm_err(ddev, "no encoders/connectors found\n");
+ ret = -EPROBE_DEFER;
+ goto unregister_cpufreq_notif;
+ }
+
+ ret = drm_vblank_init(ddev, 1);
+ if (ret < 0) {
+ drm_err(ddev, "failed to initialize vblank\n");
+ goto unregister_cpufreq_notif;
+ }
+
+ ret = platform_get_irq(pdev, 0);
+ if (ret < 0)
+ goto unregister_cpufreq_notif;
+ priv->irq = ret;
+
+ ret = tilcdc_irq_install(ddev, priv->irq);
+ if (ret < 0) {
+ drm_err(ddev, "failed to install IRQ handler\n");
+ goto unregister_cpufreq_notif;
+ }
+
+ drm_mode_config_reset(ddev);
+
+ drm_kms_helper_poll_init(ddev);
+
+ ret = drm_dev_register(ddev, 0);
+ if (ret)
+ goto stop_poll;
+
+ drm_client_setup_with_color_mode(ddev, bpp);
+
+ return 0;
+
+stop_poll:
+ drm_kms_helper_poll_fini(ddev);
+ tilcdc_irq_uninstall(ddev);
+unregister_cpufreq_notif:
+#ifdef CONFIG_CPU_FREQ
+ cpufreq_unregister_notifier(&priv->freq_transition,
+ CPUFREQ_TRANSITION_NOTIFIER);
+#endif
+disable_pm:
+ pm_runtime_disable(dev);
+ clk_put(priv->clk);
+free_wq:
+ destroy_workqueue(priv->wq);
+
+ return ret;
+}
+
+static void tilcdc_pdev_remove(struct platform_device *pdev)
+{
+ struct tilcdc_drm_private *priv = platform_get_drvdata(pdev);
+ struct drm_device *ddev = &priv->ddev;
+
+ drm_dev_unregister(ddev);
+ drm_kms_helper_poll_fini(ddev);
+ tilcdc_irq_uninstall(ddev);
+#ifdef CONFIG_CPU_FREQ
+ cpufreq_unregister_notifier(&priv->freq_transition,
+ CPUFREQ_TRANSITION_NOTIFIER);
+#endif
+ pm_runtime_disable(&pdev->dev);
+ clk_put(priv->clk);
+ destroy_workqueue(priv->wq);
+}
+
+static void tilcdc_pdev_shutdown(struct platform_device *pdev)
+{
+ struct tilcdc_drm_private *priv = platform_get_drvdata(pdev);
+ struct drm_device *ddev = &priv->ddev;
+
+ drm_atomic_helper_shutdown(ddev);
+}
+
+static const struct of_device_id tilcdc_of_match[] = {
+ { .compatible = "ti,am33xx-tilcdc", .data = (void *)AM33XX_TILCDC},
+ { .compatible = "ti,da850-tilcdc", .data = (void *)DA850_TILCDC},
+ { },
+};
+MODULE_DEVICE_TABLE(of, tilcdc_of_match);
+
+static struct platform_driver tilcdc_platform_driver = {
+ .probe = tilcdc_pdev_probe,
+ .remove = tilcdc_pdev_remove,
+ .shutdown = tilcdc_pdev_shutdown,
+ .driver = {
+ .name = "tilcdc",
+ .pm = pm_sleep_ptr(&tilcdc_pm_ops),
+ .of_match_table = tilcdc_of_match,
+ },
+};
+
+drm_module_platform_driver(tilcdc_platform_driver);
+
+MODULE_AUTHOR("Rob Clark <robdclark@gmail.com");
+MODULE_DESCRIPTION("TI LCD Controller DRM Driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/gpu/drm/tilcdc/tilcdc_drv.h b/drivers/gpu/drm/tilcdc/tilcdc_drv.h
new file mode 100644
index 000000000..17d152f9f
--- /dev/null
+++ b/drivers/gpu/drm/tilcdc/tilcdc_drv.h
@@ -0,0 +1,108 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (C) 2012 Texas Instruments
+ * Author: Rob Clark <robdclark@gmail.com>
+ */
+
+#ifndef __TILCDC_DRV_H__
+#define __TILCDC_DRV_H__
+
+#include <linux/cpufreq.h>
+#include <linux/irqreturn.h>
+
+#include <drm/drm_print.h>
+
+struct clk;
+struct workqueue_struct;
+
+struct drm_connector;
+struct drm_connector_helper_funcs;
+struct drm_crtc;
+struct drm_device;
+struct drm_display_mode;
+struct drm_encoder;
+struct drm_framebuffer;
+struct drm_minor;
+struct drm_pending_vblank_event;
+struct drm_plane;
+
+/* Defaulting to pixel clock defined on AM335x */
+#define TILCDC_DEFAULT_MAX_PIXELCLOCK 126000
+/* Maximum display width for LCDC V1 */
+#define TILCDC_DEFAULT_MAX_WIDTH_V1 1024
+/* ... and for LCDC V2 found on AM335x: */
+#define TILCDC_DEFAULT_MAX_WIDTH_V2 2048
+/*
+ * This may need some tweaking, but want to allow at least 1280x1024@60
+ * with optimized DDR & EMIF settings tweaked 1920x1080@24 appears to
+ * be supportable
+ */
+#define TILCDC_DEFAULT_MAX_BANDWIDTH (1280*1024*60)
+
+
+struct tilcdc_drm_private {
+ void __iomem *mmio;
+
+ struct clk *clk; /* functional clock */
+ int rev; /* IP revision */
+
+ unsigned int irq;
+
+ struct drm_device ddev;
+
+ /* don't attempt resolutions w/ higher W * H * Hz: */
+ uint32_t max_bandwidth;
+ /*
+ * Pixel Clock will be restricted to some value as
+ * defined in the device datasheet measured in KHz
+ */
+ uint32_t max_pixelclock;
+ /*
+ * Max allowable width is limited on a per device basis
+ * measured in pixels
+ */
+ uint32_t max_width;
+
+ u32 fifo_th;
+
+ /* Supported pixel formats */
+ const uint32_t *pixelformats;
+ uint32_t num_pixelformats;
+
+#ifdef CONFIG_CPU_FREQ
+ struct notifier_block freq_transition;
+#endif
+
+ struct workqueue_struct *wq;
+
+ struct drm_crtc *crtc;
+
+ struct tilcdc_encoder *encoder;
+ struct drm_connector *connector;
+
+ bool irq_enabled;
+};
+
+#define DBG(fmt, ...) DRM_DEBUG(fmt"\n", ##__VA_ARGS__)
+
+#define ddev_to_tilcdc_priv(x) container_of(x, struct tilcdc_drm_private, ddev)
+
+int tilcdc_crtc_create(struct drm_device *dev);
+irqreturn_t tilcdc_crtc_irq(struct drm_crtc *crtc);
+void tilcdc_crtc_update_clk(struct drm_crtc *crtc);
+void tilcdc_crtc_shutdown(struct drm_crtc *crtc);
+int tilcdc_crtc_update_fb(struct drm_crtc *crtc,
+ struct drm_framebuffer *fb,
+ struct drm_pending_vblank_event *event);
+
+struct tilcdc_plane {
+ struct drm_plane base;
+};
+
+struct tilcdc_encoder {
+ struct drm_encoder base;
+};
+
+struct tilcdc_plane *tilcdc_plane_init(struct drm_device *dev);
+
+#endif /* __TILCDC_DRV_H__ */
diff --git a/drivers/gpu/drm/tilcdc/tilcdc_encoder.c b/drivers/gpu/drm/tilcdc/tilcdc_encoder.c
new file mode 100644
index 000000000..ac4b5beb2
--- /dev/null
+++ b/drivers/gpu/drm/tilcdc/tilcdc_encoder.c
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2015 Texas Instruments
+ * Author: Jyri Sarha <jsarha@ti.com>
+ */
+
+#include <linux/of_graph.h>
+
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_bridge.h>
+#include <drm/drm_bridge_connector.h>
+#include <drm/drm_of.h>
+#include <drm/drm_simple_kms_helper.h>
+
+#include "tilcdc_drv.h"
+#include "tilcdc_encoder.h"
+
+static
+int tilcdc_attach_bridge(struct drm_device *ddev, struct drm_bridge *bridge)
+{
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(ddev);
+ struct drm_connector *connector;
+ int ret;
+
+ priv->encoder->base.possible_crtcs = BIT(0);
+
+ ret = drm_bridge_attach(&priv->encoder->base, bridge, NULL,
+ DRM_BRIDGE_ATTACH_NO_CONNECTOR);
+ if (ret)
+ return ret;
+
+ connector = drm_bridge_connector_init(ddev, &priv->encoder->base);
+ if (IS_ERR(connector)) {
+ drm_err(ddev, "bridge_connector create failed\n");
+ return PTR_ERR(connector);
+ }
+
+ priv->connector = connector;
+ return 0;
+}
+
+int tilcdc_encoder_create(struct drm_device *ddev)
+{
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(ddev);
+ struct tilcdc_encoder *encoder;
+ struct drm_bridge *bridge;
+
+ bridge = devm_drm_of_get_bridge(ddev->dev, ddev->dev->of_node, 0, 0);
+ if (PTR_ERR(bridge) == -ENODEV)
+ return 0;
+ else if (IS_ERR(bridge))
+ return PTR_ERR(bridge);
+
+ encoder = drmm_simple_encoder_alloc(ddev, struct tilcdc_encoder,
+ base, DRM_MODE_ENCODER_NONE);
+ if (IS_ERR(encoder)) {
+ drm_err(ddev, "drm_encoder_init() failed %pe\n", encoder);
+ return PTR_ERR(encoder);
+ }
+ priv->encoder = encoder;
+
+ return tilcdc_attach_bridge(ddev, bridge);
+}
diff --git a/drivers/gpu/drm/tilcdc/tilcdc_encoder.h b/drivers/gpu/drm/tilcdc/tilcdc_encoder.h
new file mode 100644
index 000000000..c8f87f590
--- /dev/null
+++ b/drivers/gpu/drm/tilcdc/tilcdc_encoder.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (C) 2015 Texas Instruments
+ * Author: Jyri Sarha <jsarha@ti.com>
+ */
+
+#ifndef __TILCDC_EXTERNAL_H__
+#define __TILCDC_EXTERNAL_H__
+
+int tilcdc_encoder_create(struct drm_device *ddev);
+#endif /* __TILCDC_SLAVE_H__ */
diff --git a/drivers/gpu/drm/tilcdc/tilcdc_panel_legacy.c b/drivers/gpu/drm/tilcdc/tilcdc_panel_legacy.c
new file mode 100644
index 000000000..7583b6ebb
--- /dev/null
+++ b/drivers/gpu/drm/tilcdc/tilcdc_panel_legacy.c
@@ -0,0 +1,185 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2025 Bootlin
+ * Author: Kory Maincent <kory.maincent@bootlin.com>
+ *
+ * To support the legacy "ti,tilcdc,panel" binding, the devicetree has to
+ * be transformed to the new panel-dpi binding with the endpoint associated.
+ */
+
+#include <linux/kernel.h>
+#include <linux/of.h>
+#include <linux/of_fdt.h>
+#include <linux/slab.h>
+
+/* Embedded dtbo symbols created by cmd_wrap_S_dtb in scripts/Makefile.lib */
+extern char __dtbo_tilcdc_panel_legacy_begin[];
+extern char __dtbo_tilcdc_panel_legacy_end[];
+
+static int __init
+tilcdc_panel_update_prop(struct of_changeset *ocs, struct device_node *node,
+ char *name, void *val, int length)
+{
+ struct property *prop;
+
+ prop = kzalloc_obj(*prop);
+ if (!prop)
+ return -ENOMEM;
+
+ prop->name = kstrdup(name, GFP_KERNEL);
+ prop->length = length;
+ prop->value = kmemdup(val, length, GFP_KERNEL);
+ if (!prop->name || !prop->value) {
+ kfree(prop->name);
+ kfree(prop->value);
+ kfree(prop);
+ return -ENOMEM;
+ }
+
+ return of_changeset_update_property(ocs, node, prop);
+}
+
+static int __init tilcdc_panel_copy_props(struct device_node *old_panel,
+ struct device_node *new_panel)
+{
+ struct device_node *old_timing __free(device_node) = NULL;
+ struct device_node *new_timing __free(device_node) = NULL;
+ struct device_node *panel_info __free(device_node) = NULL;
+ struct device_node *child __free(device_node) = NULL;
+ u32 invert_pxl_clk = 0, sync_edge = 0;
+ struct of_changeset ocs;
+ struct property *prop;
+ int ret;
+
+ child = of_get_child_by_name(old_panel, "display-timings");
+ if (!child)
+ return -EINVAL;
+
+ /* The default display timing is the one specified as native-mode.
+ * If no native-mode is specified then the first node is assumed
+ * to be the native mode.
+ */
+ old_timing = of_parse_phandle(child, "native-mode", 0);
+ if (!old_timing) {
+ old_timing = of_get_next_child(child, NULL);
+ if (!old_timing)
+ return -EINVAL;
+ }
+
+ panel_info = of_get_child_by_name(old_panel, "panel-info");
+ if (!panel_info)
+ return -EINVAL;
+
+ of_changeset_init(&ocs);
+
+ /* Copy all panel properties to the new panel node */
+ for_each_property_of_node(old_panel, prop) {
+ if (!strncmp(prop->name, "compatible", sizeof("compatible")))
+ continue;
+
+ ret = tilcdc_panel_update_prop(&ocs, new_panel, prop->name,
+ prop->value, prop->length);
+ if (ret)
+ goto destroy_ocs;
+ }
+
+ new_timing = of_changeset_create_node(&ocs, new_panel, "panel-timing");
+ if (!new_timing) {
+ ret = -ENODEV;
+ goto destroy_ocs;
+ }
+
+ /* Copy all panel timing properties to the new panel node */
+ for_each_property_of_node(old_timing, prop) {
+ ret = tilcdc_panel_update_prop(&ocs, new_timing, prop->name,
+ prop->value, prop->length);
+ if (ret)
+ goto destroy_ocs;
+ }
+
+ /* Looked only for these two parameter as all the other are always
+ * set to default and not related to common DRM properties.
+ */
+ of_property_read_u32(panel_info, "invert-pxl-clk", &invert_pxl_clk);
+ of_property_read_u32(panel_info, "sync-edge", &sync_edge);
+
+ if (!invert_pxl_clk) {
+ ret = tilcdc_panel_update_prop(&ocs, new_timing, "pixelclk-active",
+ &(__be32){cpu_to_be32(1)}, sizeof(__be32));
+ if (ret)
+ goto destroy_ocs;
+ }
+
+ if (!sync_edge) {
+ ret = tilcdc_panel_update_prop(&ocs, new_timing, "syncclk-active",
+ &(__be32){cpu_to_be32(1)}, sizeof(__be32));
+ if (ret)
+ goto destroy_ocs;
+ }
+
+ /* Remove compatible property to avoid any driver compatible match */
+ of_changeset_remove_property(&ocs, old_panel,
+ of_find_property(old_panel, "compatible", NULL));
+
+ of_changeset_apply(&ocs);
+ return 0;
+
+destroy_ocs:
+ of_changeset_destroy(&ocs);
+ return ret;
+}
+
+static const struct of_device_id tilcdc_panel_of_match[] __initconst = {
+ { .compatible = "ti,tilcdc,panel", },
+ {},
+};
+
+static const struct of_device_id tilcdc_of_match[] __initconst = {
+ { .compatible = "ti,am33xx-tilcdc", },
+ { .compatible = "ti,da850-tilcdc", },
+ {},
+};
+
+static int __init tilcdc_panel_legacy_init(void)
+{
+ struct device_node *new_panel __free(device_node) = NULL;
+ struct device_node *panel __free(device_node) = NULL;
+ struct device_node *lcdc __free(device_node) = NULL;
+ void *dtbo_start;
+ u32 dtbo_size;
+ int ovcs_id;
+ int ret;
+
+ lcdc = of_find_matching_node(NULL, tilcdc_of_match);
+ panel = of_find_matching_node(NULL, tilcdc_panel_of_match);
+
+ if (!of_device_is_available(panel) ||
+ !of_device_is_available(lcdc))
+ return 0;
+
+ dtbo_start = __dtbo_tilcdc_panel_legacy_begin;
+ dtbo_size = __dtbo_tilcdc_panel_legacy_end -
+ __dtbo_tilcdc_panel_legacy_begin;
+
+ ret = of_overlay_fdt_apply(dtbo_start, dtbo_size, &ovcs_id, NULL);
+ if (ret)
+ return ret;
+
+ new_panel = of_find_node_by_name(NULL, "tilcdc-panel-dpi");
+ if (!new_panel) {
+ ret = -ENODEV;
+ goto overlay_remove;
+ }
+
+ ret = tilcdc_panel_copy_props(panel, new_panel);
+ if (ret)
+ goto overlay_remove;
+
+ return 0;
+
+overlay_remove:
+ of_overlay_remove(&ovcs_id);
+ return ret;
+}
+
+subsys_initcall(tilcdc_panel_legacy_init);
diff --git a/drivers/gpu/drm/tilcdc/tilcdc_panel_legacy.dtso b/drivers/gpu/drm/tilcdc/tilcdc_panel_legacy.dtso
new file mode 100644
index 000000000..ae71d10f5
--- /dev/null
+++ b/drivers/gpu/drm/tilcdc/tilcdc_panel_legacy.dtso
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * DTS overlay for converting ti,tilcdc,panel binding to new binding.
+ *
+ * Copyright (C) 2025 Bootlin
+ * Author: Kory Maincent <kory.maincent@bootlin.com>
+ */
+
+/dts-v1/;
+/plugin/;
+
+&{/} {
+ tilcdc-panel-dpi {
+ compatible = "panel-dpi";
+ port {
+ panel_in: endpoint@0 {
+ remote-endpoint = <&lcd_0>;
+ };
+ };
+ };
+};
+
+&lcdc {
+ port {
+ lcd_0: endpoint@0 {
+ remote-endpoint = <&panel_in>;
+ };
+ };
+};
diff --git a/drivers/gpu/drm/tilcdc/tilcdc_plane.c b/drivers/gpu/drm/tilcdc/tilcdc_plane.c
new file mode 100644
index 000000000..d2678af5d
--- /dev/null
+++ b/drivers/gpu/drm/tilcdc/tilcdc_plane.c
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2015 Texas Instruments
+ * Author: Jyri Sarha <jsarha@ti.com>
+ */
+
+#include <drm/drm_atomic.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_fourcc.h>
+#include <drm/drm_framebuffer.h>
+
+#include "tilcdc_drv.h"
+
+static const struct drm_plane_funcs tilcdc_plane_funcs = {
+ .update_plane = drm_atomic_helper_update_plane,
+ .disable_plane = drm_atomic_helper_disable_plane,
+ .reset = drm_atomic_helper_plane_reset,
+ .atomic_duplicate_state = drm_atomic_helper_plane_duplicate_state,
+ .atomic_destroy_state = drm_atomic_helper_plane_destroy_state,
+};
+
+static int tilcdc_plane_atomic_check(struct drm_plane *plane,
+ struct drm_atomic_commit *state)
+{
+ struct drm_plane_state *new_state = drm_atomic_get_new_plane_state(state,
+ plane);
+ struct drm_crtc_state *crtc_state;
+ struct drm_plane_state *old_state = drm_atomic_get_old_plane_state(state,
+ plane);
+ unsigned int pitch;
+
+ if (!new_state->crtc)
+ return 0;
+
+ if (WARN_ON(!new_state->fb))
+ return -EINVAL;
+
+ if (new_state->crtc_x || new_state->crtc_y) {
+ drm_err(plane->dev, "%s: crtc position must be zero.",
+ __func__);
+ return -EINVAL;
+ }
+
+ crtc_state = drm_atomic_get_new_crtc_state(state, new_state->crtc);
+ /* we should have a crtc state if the plane is attached to a crtc */
+ if (WARN_ON(!crtc_state))
+ return 0;
+
+ if (crtc_state->mode.hdisplay != new_state->crtc_w ||
+ crtc_state->mode.vdisplay != new_state->crtc_h) {
+ drm_err(plane->dev,
+ "%s: Size must match mode (%dx%d == %dx%d)", __func__,
+ crtc_state->mode.hdisplay, crtc_state->mode.vdisplay,
+ new_state->crtc_w, new_state->crtc_h);
+ return -EINVAL;
+ }
+
+ pitch = crtc_state->mode.hdisplay *
+ new_state->fb->format->cpp[0];
+ if (new_state->fb->pitches[0] != pitch) {
+ drm_err(plane->dev,
+ "Invalid pitch: fb and crtc widths must be the same");
+ return -EINVAL;
+ }
+
+ if (old_state->fb && new_state->fb->format != old_state->fb->format) {
+ drm_dbg(plane->dev,
+ "%s(): pixel format change requires mode_change\n",
+ __func__);
+ crtc_state->mode_changed = true;
+ }
+
+ return 0;
+}
+
+static void tilcdc_plane_atomic_update(struct drm_plane *plane,
+ struct drm_atomic_commit *state)
+{
+ struct drm_plane_state *new_state = drm_atomic_get_new_plane_state(state,
+ plane);
+
+ if (!new_state->crtc)
+ return;
+
+ if (WARN_ON(!new_state->fb || !new_state->crtc->state))
+ return;
+
+ if (tilcdc_crtc_update_fb(new_state->crtc,
+ new_state->fb,
+ new_state->crtc->state->event) == 0) {
+ new_state->crtc->state->event = NULL;
+ }
+}
+
+static const struct drm_plane_helper_funcs plane_helper_funcs = {
+ .atomic_check = tilcdc_plane_atomic_check,
+ .atomic_update = tilcdc_plane_atomic_update,
+};
+
+struct tilcdc_plane *tilcdc_plane_init(struct drm_device *dev)
+{
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+ struct tilcdc_plane *plane;
+
+ plane = drmm_universal_plane_alloc(dev, struct tilcdc_plane, base,
+ 1, &tilcdc_plane_funcs,
+ priv->pixelformats,
+ priv->num_pixelformats,
+ NULL, DRM_PLANE_TYPE_PRIMARY, NULL);
+ if (IS_ERR(plane))
+ return plane;
+
+ drm_plane_helper_add(&plane->base, &plane_helper_funcs);
+
+ return plane;
+}
diff --git a/drivers/gpu/drm/tilcdc/tilcdc_regs.h b/drivers/gpu/drm/tilcdc/tilcdc_regs.h
new file mode 100644
index 000000000..26ebaf1e0
--- /dev/null
+++ b/drivers/gpu/drm/tilcdc/tilcdc_regs.h
@@ -0,0 +1,173 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (C) 2012 Texas Instruments
+ * Author: Rob Clark <robdclark@gmail.com>
+ */
+
+#ifndef __TILCDC_REGS_H__
+#define __TILCDC_REGS_H__
+
+/* LCDC register definitions, based on da8xx-fb */
+
+#include <linux/bitops.h>
+
+#include "tilcdc_drv.h"
+
+/* LCDC Status Register */
+#define LCDC_END_OF_FRAME1 BIT(9)
+#define LCDC_END_OF_FRAME0 BIT(8)
+#define LCDC_PL_LOAD_DONE BIT(6)
+#define LCDC_FIFO_UNDERFLOW BIT(5)
+#define LCDC_SYNC_LOST BIT(2)
+#define LCDC_FRAME_DONE BIT(0)
+
+/* LCDC DMA Control Register */
+#define LCDC_DMA_BURST_SIZE(x) ((x) << 4)
+#define LCDC_DMA_BURST_SIZE_MASK ((0x7) << 4)
+#define LCDC_DMA_BURST_1 0x0
+#define LCDC_DMA_BURST_2 0x1
+#define LCDC_DMA_BURST_4 0x2
+#define LCDC_DMA_BURST_8 0x3
+#define LCDC_DMA_BURST_16 0x4
+#define LCDC_DMA_FIFO_THRESHOLD(x) ((x) << 8)
+#define LCDC_DMA_FIFO_THRESHOLD_MASK ((0x3) << 8)
+#define LCDC_V1_END_OF_FRAME_INT_ENA BIT(2)
+#define LCDC_V2_END_OF_FRAME0_INT_ENA BIT(8)
+#define LCDC_V2_END_OF_FRAME1_INT_ENA BIT(9)
+#define LCDC_DUAL_FRAME_BUFFER_ENABLE BIT(0)
+
+/* LCDC Control Register */
+#define LCDC_CLK_DIVISOR(x) ((x) << 8)
+#define LCDC_CLK_DIVISOR_MASK ((0xFF) << 8)
+#define LCDC_RASTER_MODE 0x01
+
+/* LCDC Raster Control Register */
+#define LCDC_PALETTE_LOAD_MODE(x) ((x) << 20)
+#define LCDC_PALETTE_LOAD_MODE_MASK ((0x3) << 20)
+#define PALETTE_AND_DATA 0x00
+#define PALETTE_ONLY 0x01
+#define DATA_ONLY 0x02
+
+#define LCDC_MONO_8BIT_MODE BIT(9)
+#define LCDC_RASTER_ORDER BIT(8)
+#define LCDC_TFT_MODE BIT(7)
+#define LCDC_V1_UNDERFLOW_INT_ENA BIT(6)
+#define LCDC_V2_UNDERFLOW_INT_ENA BIT(5)
+#define LCDC_V1_PL_INT_ENA BIT(4)
+#define LCDC_V2_PL_INT_ENA BIT(6)
+#define LCDC_V1_SYNC_LOST_INT_ENA BIT(5)
+#define LCDC_V1_FRAME_DONE_INT_ENA BIT(3)
+#define LCDC_MONOCHROME_MODE BIT(1)
+#define LCDC_RASTER_ENABLE BIT(0)
+#define LCDC_TFT_ALT_ENABLE BIT(23)
+#define LCDC_STN_565_ENABLE BIT(24)
+#define LCDC_V2_DMA_CLK_EN BIT(2)
+#define LCDC_V2_LIDD_CLK_EN BIT(1)
+#define LCDC_V2_CORE_CLK_EN BIT(0)
+#define LCDC_V2_LPP_B10 26
+#define LCDC_V2_TFT_24BPP_MODE BIT(25)
+#define LCDC_V2_TFT_24BPP_UNPACK BIT(26)
+
+/* LCDC Raster Timing 2 Register */
+#define LCDC_AC_BIAS_TRANSITIONS_PER_INT(x) ((x) << 16)
+#define LCDC_AC_BIAS_TRANSITIONS_PER_INT_MASK ((0xF) << 16)
+#define LCDC_AC_BIAS_FREQUENCY(x) ((x) << 8)
+#define LCDC_AC_BIAS_FREQUENCY_MASK ((0xFF) << 8)
+#define LCDC_SYNC_CTRL BIT(25)
+#define LCDC_SYNC_EDGE BIT(24)
+#define LCDC_INVERT_PIXEL_CLOCK BIT(22)
+#define LCDC_INVERT_HSYNC BIT(21)
+#define LCDC_INVERT_VSYNC BIT(20)
+#define LCDC_LPP_B10 BIT(26)
+
+/* LCDC Block */
+#define LCDC_PID_REG 0x0
+#define LCDC_CTRL_REG 0x4
+#define LCDC_STAT_REG 0x8
+#define LCDC_RASTER_CTRL_REG 0x28
+#define LCDC_RASTER_TIMING_0_REG 0x2c
+#define LCDC_RASTER_TIMING_1_REG 0x30
+#define LCDC_RASTER_TIMING_2_REG 0x34
+#define LCDC_DMA_CTRL_REG 0x40
+#define LCDC_DMA_FB_BASE_ADDR_0_REG 0x44
+#define LCDC_DMA_FB_CEILING_ADDR_0_REG 0x48
+#define LCDC_DMA_FB_BASE_ADDR_1_REG 0x4c
+#define LCDC_DMA_FB_CEILING_ADDR_1_REG 0x50
+
+/* Interrupt Registers available only in Version 2 */
+#define LCDC_RAW_STAT_REG 0x58
+#define LCDC_MASKED_STAT_REG 0x5c
+#define LCDC_INT_ENABLE_SET_REG 0x60
+#define LCDC_INT_ENABLE_CLR_REG 0x64
+#define LCDC_END_OF_INT_IND_REG 0x68
+
+/* Clock registers available only on Version 2 */
+#define LCDC_CLK_ENABLE_REG 0x6c
+#define LCDC_CLK_RESET_REG 0x70
+#define LCDC_CLK_MAIN_RESET BIT(3)
+
+
+/*
+ * Helpers:
+ */
+
+static inline void tilcdc_write(struct drm_device *dev, u32 reg, u32 data)
+{
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+ iowrite32(data, priv->mmio + reg);
+}
+
+static inline void tilcdc_write64(struct drm_device *dev, u32 reg, u64 data)
+{
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+ volatile void __iomem *addr = priv->mmio + reg;
+
+#if defined(iowrite64) && !defined(iowrite64_is_nonatomic)
+ iowrite64(data, addr);
+#else
+ __iowmb();
+ /* This compiles to strd (=64-bit write) on ARM7 */
+ *(volatile u64 __force *)addr = __cpu_to_le64(data);
+#endif
+}
+
+static inline u32 tilcdc_read(struct drm_device *dev, u32 reg)
+{
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+ return ioread32(priv->mmio + reg);
+}
+
+static inline void tilcdc_write_mask(struct drm_device *dev, u32 reg,
+ u32 val, u32 mask)
+{
+ tilcdc_write(dev, reg, (tilcdc_read(dev, reg) & ~mask) | (val & mask));
+}
+
+static inline void tilcdc_set(struct drm_device *dev, u32 reg, u32 mask)
+{
+ tilcdc_write(dev, reg, tilcdc_read(dev, reg) | mask);
+}
+
+static inline void tilcdc_clear(struct drm_device *dev, u32 reg, u32 mask)
+{
+ tilcdc_write(dev, reg, tilcdc_read(dev, reg) & ~mask);
+}
+
+/* the register to read/clear irqstatus differs between v1 and v2 of the IP */
+static inline u32 tilcdc_irqstatus_reg(struct drm_device *dev)
+{
+ struct tilcdc_drm_private *priv = ddev_to_tilcdc_priv(dev);
+ return (priv->rev == 2) ? LCDC_MASKED_STAT_REG : LCDC_STAT_REG;
+}
+
+static inline u32 tilcdc_read_irqstatus(struct drm_device *dev)
+{
+ return tilcdc_read(dev, tilcdc_irqstatus_reg(dev));
+}
+
+static inline void tilcdc_clear_irqstatus(struct drm_device *dev, u32 mask)
+{
+ tilcdc_write(dev, tilcdc_irqstatus_reg(dev), mask);
+}
+
+#endif /* __TILCDC_REGS_H__ */