Commit 32a1795f authored by Jyri Sarha's avatar Jyri Sarha

drm/tidss: New driver for TI Keystone platform Display SubSystem

This patch adds a new DRM driver for Texas Instruments DSS IPs used on
Texas Instruments Keystone K2G, AM65x, and J721e SoCs. The new DSS IP is
a major change to the older DSS IP versions, which are supported by
the omapdrm driver. While on higher level the Keystone DSS resembles
the older DSS versions, the registers are completely different and the
internal pipelines differ a lot.

DSS IP found on K2G is an "ultra-light" version, and has only a single
plane and a single output. The K3 DSS IPs are found on AM65x and J721E
SoCs. AM65x DSS has two video ports, one full video plane, and another
"lite" plane without scaling support. J721E has 4 video ports, 2 video
planes and 2 lite planes. AM65x DSS has also an integrated OLDI (LVDS)
output.

Version history:

v2: - rebased on top of drm-next-2019-11-27
    - sort all include lines in all files
    - remove all include <drm/drmP.h>
    - remove select "select VIDEOMODE_HELPERS"
    - call dispc_vp_setup() later in tidss_crtc_atomic_flush() (there is no
      to call it in new modeset case as it is also called in vp_enable())
    - change probe sequence and drm_device allocation (follow example in
      drm_drv.c)
    - use __maybe_unused instead of #ifdef for pm functions
    - remove "struct drm_fbdev_cma *fbdev;" from driver data
    - check panel connector type before connecting it

v3: no change

v4: no change

v5: - remove fifo underflow irq handling, it is not an error and
      it should be used for debug purposes only
    - memory tuning, prefetch plane fifo up to high-threshold value to
      minimize possibility of underflows.

v6: - Check CTM and gamma support from dispc_features when creating crtc
    - Implement CTM support for k2g and fix k3 CTM implementation
    - Remove gamma property persistence and always write color properties
      in a new modeset

v7: - Fix checkpatch.pl --strict issues
    - Rebase on top of drm-misc-next-2020-01-10

v8: - Remove idle debug prints from dispc_init()
    - Add Reviewed-by: Benoit Parrot <bparrot@ti.com>

v9: - Rename dispc_write_irqenable() to dispc_set_irqenable() to avoid
      conflict exported omapfb function with same name
    - Add Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ti.com>
Co-developed-by: default avatarTomi Valkeinen <tomi.valkeinen@ti.com>
Signed-off-by: default avatarJyri Sarha <jsarha@ti.com>
Acked-by: default avatarSam Ravnborg <sam@ravnborg.org>
Reviewed-by: default avatarBenoit Parrot <bparrot@ti.com>
Signed-off-by: default avatarTomi Valkeinen <tomi.valkeinen@ti.com>
Link: https://patchwork.freedesktop.org/patch/msgid/925fbfad58ff828e8e07fdff7073a0ee65750c3d.1580129724.git.jsarha@ti.com
parent 6057317c
...@@ -389,6 +389,8 @@ source "drivers/gpu/drm/aspeed/Kconfig" ...@@ -389,6 +389,8 @@ source "drivers/gpu/drm/aspeed/Kconfig"
source "drivers/gpu/drm/mcde/Kconfig" source "drivers/gpu/drm/mcde/Kconfig"
source "drivers/gpu/drm/tidss/Kconfig"
# Keep legacy drivers last # Keep legacy drivers last
menuconfig DRM_LEGACY menuconfig DRM_LEGACY
......
...@@ -122,3 +122,4 @@ obj-$(CONFIG_DRM_LIMA) += lima/ ...@@ -122,3 +122,4 @@ obj-$(CONFIG_DRM_LIMA) += lima/
obj-$(CONFIG_DRM_PANFROST) += panfrost/ obj-$(CONFIG_DRM_PANFROST) += panfrost/
obj-$(CONFIG_DRM_ASPEED_GFX) += aspeed/ obj-$(CONFIG_DRM_ASPEED_GFX) += aspeed/
obj-$(CONFIG_DRM_MCDE) += mcde/ obj-$(CONFIG_DRM_MCDE) += mcde/
obj-$(CONFIG_DRM_TIDSS) += tidss/
config DRM_TIDSS
tristate "DRM Support for TI Keystone"
depends on DRM && OF
depends on ARM || ARM64 || COMPILE_TEST
select DRM_KMS_HELPER
select DRM_KMS_CMA_HELPER
select DRM_GEM_CMA_HELPER
help
The TI Keystone family SoCs introduced a new generation of
Display SubSystem. There is currently three Keystone family
SoCs released with DSS. Each with somewhat different version
of it. The SoCs are 66AK2Gx, AM65x, and J721E. Set this to Y
or M to add display support for TI Keystone family
platforms.
# SPDX-License-Identifier: GPL-2.0
tidss-y := tidss_crtc.o \
tidss_drv.o \
tidss_encoder.o \
tidss_kms.o \
tidss_irq.o \
tidss_plane.o \
tidss_scale_coefs.o \
tidss_dispc.o
obj-$(CONFIG_DRM_TIDSS) += tidss.o
This diff is collapsed.
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (C) 2018 Texas Instruments Incorporated - http://www.ti.com/
* Author: Tomi Valkeinen <tomi.valkeinen@ti.com>
*/
#ifndef __TIDSS_CRTC_H__
#define __TIDSS_CRTC_H__
#include <linux/completion.h>
#include <linux/wait.h>
#include <drm/drm_crtc.h>
#define to_tidss_crtc(c) container_of((c), struct tidss_crtc, crtc)
struct tidss_device;
struct tidss_crtc {
struct drm_crtc crtc;
u32 hw_videoport;
struct drm_pending_vblank_event *event;
struct completion framedone_completion;
};
#define to_tidss_crtc_state(x) container_of(x, struct tidss_crtc_state, base)
struct tidss_crtc_state {
/* Must be first. */
struct drm_crtc_state base;
u32 bus_format;
u32 bus_flags;
};
void tidss_crtc_vblank_irq(struct drm_crtc *crtc);
void tidss_crtc_framedone_irq(struct drm_crtc *crtc);
void tidss_crtc_error_irq(struct drm_crtc *crtc, u64 irqstatus);
struct tidss_crtc *tidss_crtc_create(struct tidss_device *tidss,
u32 hw_videoport,
struct drm_plane *primary);
#endif
This diff is collapsed.
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (C) 2018 Texas Instruments Incorporated - http://www.ti.com/
* Author: Tomi Valkeinen <tomi.valkeinen@ti.com>
*/
#ifndef __TIDSS_DISPC_H__
#define __TIDSS_DISPC_H__
#include "tidss_drv.h"
struct dispc_device;
struct drm_crtc_state;
enum tidss_gamma_type { TIDSS_GAMMA_8BIT, TIDSS_GAMMA_10BIT };
struct tidss_vp_feat {
struct tidss_vp_color_feat {
u32 gamma_size;
enum tidss_gamma_type gamma_type;
bool has_ctm;
} color;
};
struct tidss_plane_feat {
struct tidss_plane_color_feat {
u32 encodings;
u32 ranges;
enum drm_color_encoding default_encoding;
enum drm_color_range default_range;
} color;
struct tidss_plane_blend_feat {
bool global_alpha;
} blend;
};
struct dispc_features_scaling {
u32 in_width_max_5tap_rgb;
u32 in_width_max_3tap_rgb;
u32 in_width_max_5tap_yuv;
u32 in_width_max_3tap_yuv;
u32 upscale_limit;
u32 downscale_limit_5tap;
u32 downscale_limit_3tap;
u32 xinc_max;
};
struct dispc_errata {
bool i2000; /* DSS Does Not Support YUV Pixel Data Formats */
};
enum dispc_vp_bus_type {
DISPC_VP_DPI, /* DPI output */
DISPC_VP_OLDI, /* OLDI (LVDS) output */
DISPC_VP_INTERNAL, /* SoC internal routing */
DISPC_VP_MAX_BUS_TYPE,
};
enum dispc_dss_subrevision {
DISPC_K2G,
DISPC_AM65X,
DISPC_J721E,
};
struct dispc_features {
int min_pclk_khz;
int max_pclk_khz[DISPC_VP_MAX_BUS_TYPE];
struct dispc_features_scaling scaling;
enum dispc_dss_subrevision subrev;
const char *common;
const u16 *common_regs;
u32 num_vps;
const char *vp_name[TIDSS_MAX_PORTS]; /* Should match dt reg names */
const char *ovr_name[TIDSS_MAX_PORTS]; /* Should match dt reg names */
const char *vpclk_name[TIDSS_MAX_PORTS]; /* Should match dt clk names */
const enum dispc_vp_bus_type vp_bus_type[TIDSS_MAX_PORTS];
struct tidss_vp_feat vp_feat;
u32 num_planes;
const char *vid_name[TIDSS_MAX_PLANES]; /* Should match dt reg names */
bool vid_lite[TIDSS_MAX_PLANES];
u32 vid_order[TIDSS_MAX_PLANES];
struct dispc_errata errata;
};
extern const struct dispc_features dispc_k2g_feats;
extern const struct dispc_features dispc_am65x_feats;
extern const struct dispc_features dispc_j721e_feats;
void dispc_set_irqenable(struct dispc_device *dispc, dispc_irq_t mask);
dispc_irq_t dispc_read_and_clear_irqstatus(struct dispc_device *dispc);
void dispc_vp_prepare(struct dispc_device *dispc, u32 hw_videoport,
const struct drm_crtc_state *state);
void dispc_vp_enable(struct dispc_device *dispc, u32 hw_videoport,
const struct drm_crtc_state *state);
void dispc_vp_disable(struct dispc_device *dispc, u32 hw_videoport);
void dispc_vp_unprepare(struct dispc_device *dispc, u32 hw_videoport);
bool dispc_vp_go_busy(struct dispc_device *dispc, u32 hw_videoport);
void dispc_vp_go(struct dispc_device *dispc, u32 hw_videoport);
int dispc_vp_bus_check(struct dispc_device *dispc, u32 hw_videoport,
const struct drm_crtc_state *state);
enum drm_mode_status dispc_vp_mode_valid(struct dispc_device *dispc,
u32 hw_videoport,
const struct drm_display_mode *mode);
int dispc_vp_enable_clk(struct dispc_device *dispc, u32 hw_videoport);
void dispc_vp_disable_clk(struct dispc_device *dispc, u32 hw_videoport);
int dispc_vp_set_clk_rate(struct dispc_device *dispc, u32 hw_videoport,
unsigned long rate);
void dispc_vp_setup(struct dispc_device *dispc, u32 hw_videoport,
const struct drm_crtc_state *state, bool newmodeset);
int dispc_runtime_suspend(struct dispc_device *dispc);
int dispc_runtime_resume(struct dispc_device *dispc);
int dispc_plane_check(struct dispc_device *dispc, u32 hw_plane,
const struct drm_plane_state *state,
u32 hw_videoport);
int dispc_plane_setup(struct dispc_device *dispc, u32 hw_plane,
const struct drm_plane_state *state,
u32 hw_videoport);
int dispc_plane_enable(struct dispc_device *dispc, u32 hw_plane, bool enable);
const u32 *dispc_plane_formats(struct dispc_device *dispc, unsigned int *len);
int dispc_init(struct tidss_device *tidss);
void dispc_remove(struct tidss_device *tidss);
#endif
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (C) 2016-2018 Texas Instruments Incorporated - http://www.ti.com/
* Author: Jyri Sarha <jsarha@ti.com>
*/
#ifndef __TIDSS_DISPC_REGS_H
#define __TIDSS_DISPC_REGS_H
enum dispc_common_regs {
NOT_APPLICABLE_OFF = 0,
DSS_REVISION_OFF,
DSS_SYSCONFIG_OFF,
DSS_SYSSTATUS_OFF,
DISPC_IRQ_EOI_OFF,
DISPC_IRQSTATUS_RAW_OFF,
DISPC_IRQSTATUS_OFF,
DISPC_IRQENABLE_SET_OFF,
DISPC_IRQENABLE_CLR_OFF,
DISPC_VID_IRQENABLE_OFF,
DISPC_VID_IRQSTATUS_OFF,
DISPC_VP_IRQENABLE_OFF,
DISPC_VP_IRQSTATUS_OFF,
WB_IRQENABLE_OFF,
WB_IRQSTATUS_OFF,
DISPC_GLOBAL_MFLAG_ATTRIBUTE_OFF,
DISPC_GLOBAL_OUTPUT_ENABLE_OFF,
DISPC_GLOBAL_BUFFER_OFF,
DSS_CBA_CFG_OFF,
DISPC_DBG_CONTROL_OFF,
DISPC_DBG_STATUS_OFF,
DISPC_CLKGATING_DISABLE_OFF,
DISPC_SECURE_DISABLE_OFF,
FBDC_REVISION_1_OFF,
FBDC_REVISION_2_OFF,
FBDC_REVISION_3_OFF,
FBDC_REVISION_4_OFF,
FBDC_REVISION_5_OFF,
FBDC_REVISION_6_OFF,
FBDC_COMMON_CONTROL_OFF,
FBDC_CONSTANT_COLOR_0_OFF,
FBDC_CONSTANT_COLOR_1_OFF,
DISPC_CONNECTIONS_OFF,
DISPC_MSS_VP1_OFF,
DISPC_MSS_VP3_OFF,
DISPC_COMMON_REG_TABLE_LEN,
};
/*
* dispc_common_regmap should be defined as const u16 * and pointing
* to a valid dss common register map for the platform, before the
* macros bellow can be used.
*/
#define REG(r) (dispc_common_regmap[r ## _OFF])
#define DSS_REVISION REG(DSS_REVISION)
#define DSS_SYSCONFIG REG(DSS_SYSCONFIG)
#define DSS_SYSSTATUS REG(DSS_SYSSTATUS)
#define DISPC_IRQ_EOI REG(DISPC_IRQ_EOI)
#define DISPC_IRQSTATUS_RAW REG(DISPC_IRQSTATUS_RAW)
#define DISPC_IRQSTATUS REG(DISPC_IRQSTATUS)
#define DISPC_IRQENABLE_SET REG(DISPC_IRQENABLE_SET)
#define DISPC_IRQENABLE_CLR REG(DISPC_IRQENABLE_CLR)
#define DISPC_VID_IRQENABLE(n) (REG(DISPC_VID_IRQENABLE) + (n) * 4)
#define DISPC_VID_IRQSTATUS(n) (REG(DISPC_VID_IRQSTATUS) + (n) * 4)
#define DISPC_VP_IRQENABLE(n) (REG(DISPC_VP_IRQENABLE) + (n) * 4)
#define DISPC_VP_IRQSTATUS(n) (REG(DISPC_VP_IRQSTATUS) + (n) * 4)
#define WB_IRQENABLE REG(WB_IRQENABLE)
#define WB_IRQSTATUS REG(WB_IRQSTATUS)
#define DISPC_GLOBAL_MFLAG_ATTRIBUTE REG(DISPC_GLOBAL_MFLAG_ATTRIBUTE)
#define DISPC_GLOBAL_OUTPUT_ENABLE REG(DISPC_GLOBAL_OUTPUT_ENABLE)
#define DISPC_GLOBAL_BUFFER REG(DISPC_GLOBAL_BUFFER)
#define DSS_CBA_CFG REG(DSS_CBA_CFG)
#define DISPC_DBG_CONTROL REG(DISPC_DBG_CONTROL)
#define DISPC_DBG_STATUS REG(DISPC_DBG_STATUS)
#define DISPC_CLKGATING_DISABLE REG(DISPC_CLKGATING_DISABLE)
#define DISPC_SECURE_DISABLE REG(DISPC_SECURE_DISABLE)
#define FBDC_REVISION_1 REG(FBDC_REVISION_1)
#define FBDC_REVISION_2 REG(FBDC_REVISION_2)
#define FBDC_REVISION_3 REG(FBDC_REVISION_3)
#define FBDC_REVISION_4 REG(FBDC_REVISION_4)
#define FBDC_REVISION_5 REG(FBDC_REVISION_5)
#define FBDC_REVISION_6 REG(FBDC_REVISION_6)
#define FBDC_COMMON_CONTROL REG(FBDC_COMMON_CONTROL)
#define FBDC_CONSTANT_COLOR_0 REG(FBDC_CONSTANT_COLOR_0)
#define FBDC_CONSTANT_COLOR_1 REG(FBDC_CONSTANT_COLOR_1)
#define DISPC_CONNECTIONS REG(DISPC_CONNECTIONS)
#define DISPC_MSS_VP1 REG(DISPC_MSS_VP1)
#define DISPC_MSS_VP3 REG(DISPC_MSS_VP3)
/* VID */
#define DISPC_VID_ACCUH_0 0x0
#define DISPC_VID_ACCUH_1 0x4
#define DISPC_VID_ACCUH2_0 0x8
#define DISPC_VID_ACCUH2_1 0xc
#define DISPC_VID_ACCUV_0 0x10
#define DISPC_VID_ACCUV_1 0x14
#define DISPC_VID_ACCUV2_0 0x18
#define DISPC_VID_ACCUV2_1 0x1c
#define DISPC_VID_ATTRIBUTES 0x20
#define DISPC_VID_ATTRIBUTES2 0x24
#define DISPC_VID_BA_0 0x28
#define DISPC_VID_BA_1 0x2c
#define DISPC_VID_BA_UV_0 0x30
#define DISPC_VID_BA_UV_1 0x34
#define DISPC_VID_BUF_SIZE_STATUS 0x38
#define DISPC_VID_BUF_THRESHOLD 0x3c
#define DISPC_VID_CSC_COEF(n) (0x40 + (n) * 4)
#define DISPC_VID_FIRH 0x5c
#define DISPC_VID_FIRH2 0x60
#define DISPC_VID_FIRV 0x64
#define DISPC_VID_FIRV2 0x68
#define DISPC_VID_FIR_COEFS_H0 0x6c
#define DISPC_VID_FIR_COEF_H0(phase) (0x6c + (phase) * 4)
#define DISPC_VID_FIR_COEFS_H0_C 0x90
#define DISPC_VID_FIR_COEF_H0_C(phase) (0x90 + (phase) * 4)
#define DISPC_VID_FIR_COEFS_H12 0xb4
#define DISPC_VID_FIR_COEF_H12(phase) (0xb4 + (phase) * 4)
#define DISPC_VID_FIR_COEFS_H12_C 0xf4
#define DISPC_VID_FIR_COEF_H12_C(phase) (0xf4 + (phase) * 4)
#define DISPC_VID_FIR_COEFS_V0 0x134
#define DISPC_VID_FIR_COEF_V0(phase) (0x134 + (phase) * 4)
#define DISPC_VID_FIR_COEFS_V0_C 0x158
#define DISPC_VID_FIR_COEF_V0_C(phase) (0x158 + (phase) * 4)
#define DISPC_VID_FIR_COEFS_V12 0x17c
#define DISPC_VID_FIR_COEF_V12(phase) (0x17c + (phase) * 4)
#define DISPC_VID_FIR_COEFS_V12_C 0x1bc
#define DISPC_VID_FIR_COEF_V12_C(phase) (0x1bc + (phase) * 4)
#define DISPC_VID_GLOBAL_ALPHA 0x1fc
#define DISPC_VID_K2G_IRQENABLE 0x200 /* K2G */
#define DISPC_VID_K2G_IRQSTATUS 0x204 /* K2G */
#define DISPC_VID_MFLAG_THRESHOLD 0x208
#define DISPC_VID_PICTURE_SIZE 0x20c
#define DISPC_VID_PIXEL_INC 0x210
#define DISPC_VID_K2G_POSITION 0x214 /* K2G */
#define DISPC_VID_PRELOAD 0x218
#define DISPC_VID_ROW_INC 0x21c
#define DISPC_VID_SIZE 0x220
#define DISPC_VID_BA_EXT_0 0x22c
#define DISPC_VID_BA_EXT_1 0x230
#define DISPC_VID_BA_UV_EXT_0 0x234
#define DISPC_VID_BA_UV_EXT_1 0x238
#define DISPC_VID_CSC_COEF7 0x23c
#define DISPC_VID_ROW_INC_UV 0x248
#define DISPC_VID_CLUT 0x260
#define DISPC_VID_SAFETY_ATTRIBUTES 0x2a0
#define DISPC_VID_SAFETY_CAPT_SIGNATURE 0x2a4
#define DISPC_VID_SAFETY_POSITION 0x2a8
#define DISPC_VID_SAFETY_REF_SIGNATURE 0x2ac
#define DISPC_VID_SAFETY_SIZE 0x2b0
#define DISPC_VID_SAFETY_LFSR_SEED 0x2b4
#define DISPC_VID_LUMAKEY 0x2b8
#define DISPC_VID_DMA_BUFSIZE 0x2bc /* J721E */
/* OVR */
#define DISPC_OVR_CONFIG 0x0
#define DISPC_OVR_VIRTVP 0x4 /* J721E */
#define DISPC_OVR_DEFAULT_COLOR 0x8
#define DISPC_OVR_DEFAULT_COLOR2 0xc
#define DISPC_OVR_TRANS_COLOR_MAX 0x10
#define DISPC_OVR_TRANS_COLOR_MAX2 0x14
#define DISPC_OVR_TRANS_COLOR_MIN 0x18
#define DISPC_OVR_TRANS_COLOR_MIN2 0x1c
#define DISPC_OVR_ATTRIBUTES(n) (0x20 + (n) * 4)
#define DISPC_OVR_ATTRIBUTES2(n) (0x34 + (n) * 4) /* J721E */
/* VP */
#define DISPC_VP_CONFIG 0x0
#define DISPC_VP_CONTROL 0x4
#define DISPC_VP_CSC_COEF0 0x8
#define DISPC_VP_CSC_COEF1 0xc
#define DISPC_VP_CSC_COEF2 0x10
#define DISPC_VP_DATA_CYCLE_0 0x14
#define DISPC_VP_DATA_CYCLE_1 0x18
#define DISPC_VP_K2G_GAMMA_TABLE 0x20 /* K2G */
#define DISPC_VP_K2G_IRQENABLE 0x3c /* K2G */
#define DISPC_VP_K2G_IRQSTATUS 0x40 /* K2G */
#define DISPC_VP_DATA_CYCLE_2 0x1c
#define DISPC_VP_LINE_NUMBER 0x44
#define DISPC_VP_POL_FREQ 0x4c
#define DISPC_VP_SIZE_SCREEN 0x50
#define DISPC_VP_TIMING_H 0x54
#define DISPC_VP_TIMING_V 0x58
#define DISPC_VP_CSC_COEF3 0x5c
#define DISPC_VP_CSC_COEF4 0x60
#define DISPC_VP_CSC_COEF5 0x64
#define DISPC_VP_CSC_COEF6 0x68
#define DISPC_VP_CSC_COEF7 0x6c
#define DISPC_VP_SAFETY_ATTRIBUTES_0 0x70
#define DISPC_VP_SAFETY_ATTRIBUTES_1 0x74
#define DISPC_VP_SAFETY_ATTRIBUTES_2 0x78
#define DISPC_VP_SAFETY_ATTRIBUTES_3 0x7c
#define DISPC_VP_SAFETY_CAPT_SIGNATURE_0 0x90
#define DISPC_VP_SAFETY_CAPT_SIGNATURE_1 0x94
#define DISPC_VP_SAFETY_CAPT_SIGNATURE_2 0x98
#define DISPC_VP_SAFETY_CAPT_SIGNATURE_3 0x9c
#define DISPC_VP_SAFETY_POSITION_0 0xb0
#define DISPC_VP_SAFETY_POSITION_1 0xb4
#define DISPC_VP_SAFETY_POSITION_2 0xb8
#define DISPC_VP_SAFETY_POSITION_3 0xbc
#define DISPC_VP_SAFETY_REF_SIGNATURE_0 0xd0
#define DISPC_VP_SAFETY_REF_SIGNATURE_1 0xd4
#define DISPC_VP_SAFETY_REF_SIGNATURE_2 0xd8
#define DISPC_VP_SAFETY_REF_SIGNATURE_3 0xdc
#define DISPC_VP_SAFETY_SIZE_0 0xf0
#define DISPC_VP_SAFETY_SIZE_1 0xf4
#define DISPC_VP_SAFETY_SIZE_2 0xf8
#define DISPC_VP_SAFETY_SIZE_3 0xfc
#define DISPC_VP_SAFETY_LFSR_SEED 0x110
#define DISPC_VP_GAMMA_TABLE 0x120
#define DISPC_VP_DSS_OLDI_CFG 0x160
#define DISPC_VP_DSS_OLDI_STATUS 0x164
#define DISPC_VP_DSS_OLDI_LB 0x168
#define DISPC_VP_DSS_MERGE_SPLIT 0x16c /* J721E */
#define DISPC_VP_DSS_DMA_THREADSIZE 0x170 /* J721E */
#define DISPC_VP_DSS_DMA_THREADSIZE_STATUS 0x174 /* J721E */
/*
* OLDI IO_CTRL register offsets. On AM654 the registers are found
* from CTRL_MMR0, there the syscon regmap should map 0x14 bytes from
* CTRLMMR0P1_OLDI_DAT0_IO_CTRL to CTRLMMR0P1_OLDI_CLK_IO_CTRL
* register range.
*/
#define OLDI_DAT0_IO_CTRL 0x00
#define OLDI_DAT1_IO_CTRL 0x04
#define OLDI_DAT2_IO_CTRL 0x08
#define OLDI_DAT3_IO_CTRL 0x0C
#define OLDI_CLK_IO_CTRL 0x10
#define OLDI_PWRDN_TX BIT(8)
#endif /* __TIDSS_DISPC_REGS_H */
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (C) 2018 Texas Instruments Incorporated - http://www.ti.com/
* Author: Tomi Valkeinen <tomi.valkeinen@ti.com>
*/
#include <linux/console.h>
#include <linux/of_device.h>
#include <linux/module.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_crtc_helper.h>
#include <drm/drm_drv.h>
#include <drm/drm_fb_helper.h>
#include <drm/drm_gem_cma_helper.h>
#include <drm/drm_irq.h>
#include <drm/drm_probe_helper.h>
#include "tidss_dispc.h"
#include "tidss_drv.h"
#include "tidss_kms.h"
#include "tidss_irq.h"
/* Power management */
int tidss_runtime_get(struct tidss_device *tidss)
{
int r;
dev_dbg(tidss->dev, "%s\n", __func__);
r = pm_runtime_get_sync(tidss->dev);
WARN_ON(r < 0);
return r < 0 ? r : 0;
}
void tidss_runtime_put(struct tidss_device *tidss)
{
int r;
dev_dbg(tidss->dev, "%s\n", __func__);
r = pm_runtime_put_sync(tidss->dev);
WARN_ON(r < 0);
}
static int __maybe_unused tidss_pm_runtime_suspend(struct device *dev)
{
struct tidss_device *tidss = dev_get_drvdata(dev);
dev_dbg(dev, "%s\n", __func__);
return dispc_runtime_suspend(tidss->dispc);
}
static int __maybe_unused tidss_pm_runtime_resume(struct device *dev)
{
struct tidss_device *tidss = dev_get_drvdata(dev);
int r;
dev_dbg(dev, "%s\n", __func__);
r = dispc_runtime_resume(tidss->dispc);
if (r)
return r;
return 0;
}
static int __maybe_unused tidss_suspend(struct device *dev)
{
struct tidss_device *tidss = dev_get_drvdata(dev);
dev_dbg(dev, "%s\n", __func__);
return drm_mode_config_helper_suspend(&tidss->ddev);
}
static int __maybe_unused tidss_resume(struct device *dev)
{
struct tidss_device *tidss = dev_get_drvdata(dev);
dev_dbg(dev, "%s\n", __func__);
return drm_mode_config_helper_resume(&tidss->ddev);
}
#ifdef CONFIG_PM
static const struct dev_pm_ops tidss_pm_ops = {
.runtime_suspend = tidss_pm_runtime_suspend,
.runtime_resume = tidss_pm_runtime_resume,
SET_SYSTEM_SLEEP_PM_OPS(tidss_suspend, tidss_resume)
};
#endif /* CONFIG_PM */
/* DRM device Information */
static void tidss_release(struct drm_device *ddev)
{
struct tidss_device *tidss = ddev->dev_private;
drm_kms_helper_poll_fini(ddev);
tidss_modeset_cleanup(tidss);
drm_dev_fini(ddev);
kfree(tidss);
}
DEFINE_DRM_GEM_CMA_FOPS(tidss_fops);
static struct drm_driver tidss_driver = {
.driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_ATOMIC,
.fops = &tidss_fops,
.release = tidss_release,
DRM_GEM_CMA_VMAP_DRIVER_OPS,
.name = "tidss",
.desc = "TI Keystone DSS",
.date = "20180215",
.major = 1,
.minor = 0,
.irq_preinstall = tidss_irq_preinstall,
.irq_postinstall = tidss_irq_postinstall,
.irq_handler = tidss_irq_handler,
.irq_uninstall = tidss_irq_uninstall,
};
static int tidss_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct tidss_device *tidss;
struct drm_device *ddev;
int ret;
int irq;
dev_dbg(dev, "%s\n", __func__);
/* Can't use devm_* since drm_device's lifetime may exceed dev's */
tidss = kzalloc(sizeof(*tidss), GFP_KERNEL);
if (!tidss)
return -ENOMEM;
ddev = &tidss->ddev;
ret = devm_drm_dev_init(&pdev->dev, ddev, &tidss_driver);
if (ret) {
kfree(ddev);
return ret;
}
tidss->dev = dev;
tidss->feat = of_device_get_match_data(dev);
platform_set_drvdata(pdev, tidss);
ddev->dev_private = tidss;
ret = dispc_init(tidss);
if (ret) {
dev_err(dev, "failed to initialize dispc: %d\n", ret);
return ret;
}
pm_runtime_enable(dev);
#ifndef CONFIG_PM
/* If we don't have PM, we need to call resume manually */
dispc_runtime_resume(tidss->dispc);
#endif
ret = tidss_modeset_init(tidss);
if (ret < 0) {
if (ret != -EPROBE_DEFER)
dev_err(dev, "failed to init DRM/KMS (%d)\n", ret);
goto err_runtime_suspend;
}
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
ret = irq;
goto err_runtime_suspend;
}
ret = drm_irq_install(ddev, irq);
if (ret) {
dev_err(dev, "drm_irq_install failed: %d\n", ret);
goto err_runtime_suspend;
}
drm_kms_helper_poll_init(ddev);
drm_mode_config_reset(ddev);
ret = drm_dev_register(ddev, 0);
if (ret) {
dev_err(dev, "failed to register DRM device\n");
goto err_irq_uninstall;
}
drm_fbdev_generic_setup(ddev, 32);
dev_dbg(dev, "%s done\n", __func__);
return 0;
err_irq_uninstall:
drm_irq_uninstall(ddev);
err_runtime_suspend:
#ifndef CONFIG_PM
dispc_runtime_suspend(tidss->dispc);
#endif
pm_runtime_disable(dev);
return ret;
}
static int tidss_remove(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct tidss_device *tidss = platform_get_drvdata(pdev);
struct drm_device *ddev = &tidss->ddev;
dev_dbg(dev, "%s\n", __func__);
drm_dev_unregister(ddev);
drm_atomic_helper_shutdown(ddev);
drm_irq_uninstall(ddev);
#ifndef CONFIG_PM
/* If we don't have PM, we need to call suspend manually */
dispc_runtime_suspend(tidss->dispc);
#endif
pm_runtime_disable(dev);
/* devm allocated dispc goes away with the dev so mark it NULL */
dispc_remove(tidss);
dev_dbg(dev, "%s done\n", __func__);
return 0;
}
static void tidss_shutdown(struct platform_device *pdev)
{
drm_atomic_helper_shutdown(platform_get_drvdata(pdev));
}
static const struct of_device_id tidss_of_table[] = {
{ .compatible = "ti,k2g-dss", .data = &dispc_k2g_feats, },
{ .compatible = "ti,am65x-dss", .data = &dispc_am65x_feats, },
{ .compatible = "ti,j721e-dss", .data = &dispc_j721e_feats, },
{ }
};
MODULE_DEVICE_TABLE(of, tidss_of_table);
static struct platform_driver tidss_platform_driver = {
.probe = tidss_probe,
.remove = tidss_remove,
.shutdown = tidss_shutdown,
.driver = {
.name = "tidss",
#ifdef CONFIG_PM
.pm = &tidss_pm_ops,
#endif
.of_match_table = tidss_of_table,
.suppress_bind_attrs = true,
},
};
module_platform_driver(tidss_platform_driver);
MODULE_AUTHOR("Tomi Valkeinen <tomi.valkeinen@ti.com>");
MODULE_DESCRIPTION("TI Keystone DSS Driver");
MODULE_LICENSE("GPL v2");
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (C) 2018 Texas Instruments Incorporated - http://www.ti.com/
* Author: Tomi Valkeinen <tomi.valkeinen@ti.com>
*/
#ifndef __TIDSS_DRV_H__
#define __TIDSS_DRV_H__
#include <linux/spinlock.h>
#define TIDSS_MAX_PORTS 4
#define TIDSS_MAX_PLANES 4
typedef u32 dispc_irq_t;
struct tidss_device {
struct drm_device ddev; /* DRM device for DSS */
struct device *dev; /* Underlying DSS device */
const struct dispc_features *feat;
struct dispc_device *dispc;
unsigned int num_crtcs;
struct drm_crtc *crtcs[TIDSS_MAX_PORTS];
unsigned int num_planes;
struct drm_plane *planes[TIDSS_MAX_PLANES];
spinlock_t wait_lock; /* protects the irq masks */
dispc_irq_t irq_mask; /* enabled irqs in addition to wait_list */
struct drm_atomic_state *saved_state;
};
int tidss_runtime_get(struct tidss_device *tidss);
void tidss_runtime_put(struct tidss_device *tidss);
#endif
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (C) 2018 Texas Instruments Incorporated - http://www.ti.com/
* Author: Tomi Valkeinen <tomi.valkeinen@ti.com>
*/
#include <linux/export.h>
#include <drm/drm_crtc.h>
#include <drm/drm_crtc_helper.h>
#include <drm/drm_panel.h>
#include <drm/drm_of.h>
#include "tidss_crtc.h"
#include "tidss_drv.h"
#include "tidss_encoder.h"
static int tidss_encoder_atomic_check(struct drm_encoder *encoder,
struct drm_crtc_state *crtc_state,
struct drm_connector_state *conn_state)
{
struct drm_device *ddev = encoder->dev;
struct tidss_crtc_state *tcrtc_state = to_tidss_crtc_state(crtc_state);
struct drm_display_info *di = &conn_state->connector->display_info;
struct drm_bridge *bridge;
bool bus_flags_set = false;
dev_dbg(ddev->dev, "%s\n", __func__);
/*
* Take the bus_flags from the first bridge that defines
* bridge timings, or from the connector's display_info if no
* bridge defines the timings.
*/
list_for_each_entry(bridge, &encoder->bridge_chain, chain_node) {
if (!bridge->timings)
continue;
tcrtc_state->bus_flags = bridge->timings->input_bus_flags;
bus_flags_set = true;
break;
}
if (!di->bus_formats || di->num_bus_formats == 0) {
dev_err(ddev->dev, "%s: No bus_formats in connected display\n",
__func__);
return -EINVAL;
}
// XXX any cleaner way to set bus format and flags?
tcrtc_state->bus_format = di->bus_formats[0];
if (!bus_flags_set)
tcrtc_state->bus_flags = di->bus_flags;
return 0;
}
static const struct drm_encoder_helper_funcs encoder_helper_funcs = {
.atomic_check = tidss_encoder_atomic_check,
};
static const struct drm_encoder_funcs encoder_funcs = {
.destroy = drm_encoder_cleanup,
};
struct drm_encoder *tidss_encoder_create(struct tidss_device *tidss,
u32 encoder_type, u32 possible_crtcs)
{
struct drm_encoder *enc;
int ret;
enc = devm_kzalloc(tidss->dev, sizeof(*enc), GFP_KERNEL);
if (!enc)
return ERR_PTR(-ENOMEM);
enc->possible_crtcs = possible_crtcs;
ret = drm_encoder_init(&tidss->ddev, enc, &encoder_funcs,
encoder_type, NULL);
if (ret < 0)
return ERR_PTR(ret);
drm_encoder_helper_add(enc, &encoder_helper_funcs);
dev_dbg(tidss->dev, "Encoder create done\n");
return enc;
}
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (C) 2018 Texas Instruments Incorporated - http://www.ti.com/
* Author: Tomi Valkeinen <tomi.valkeinen@ti.com>
*/
#ifndef __TIDSS_ENCODER_H__
#define __TIDSS_ENCODER_H__
#include <drm/drm_encoder.h>
struct tidss_device;
struct drm_encoder *tidss_encoder_create(struct tidss_device *tidss,
u32 encoder_type, u32 possible_crtcs);
#endif
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (C) 2018 Texas Instruments Incorporated - http://www.ti.com/
* Author: Tomi Valkeinen <tomi.valkeinen@ti.com>
*/
#include <drm/drm_print.h>
#include "tidss_crtc.h"
#include "tidss_dispc.h"
#include "tidss_drv.h"
#include "tidss_irq.h"
#include "tidss_plane.h"
/* call with wait_lock and dispc runtime held */
static void tidss_irq_update(struct tidss_device *tidss)
{
assert_spin_locked(&tidss->wait_lock);
dispc_set_irqenable(tidss->dispc, tidss->irq_mask);
}
void tidss_irq_enable_vblank(struct drm_crtc *crtc)
{
struct drm_device *ddev = crtc->dev;
struct tidss_device *tidss = ddev->dev_private;
struct tidss_crtc *tcrtc = to_tidss_crtc(crtc);
u32 hw_videoport = tcrtc->hw_videoport;
unsigned long flags;
spin_lock_irqsave(&tidss->wait_lock, flags);
tidss->irq_mask |= DSS_IRQ_VP_VSYNC_EVEN(hw_videoport) |
DSS_IRQ_VP_VSYNC_ODD(hw_videoport);
tidss_irq_update(tidss);
spin_unlock_irqrestore(&tidss->wait_lock, flags);
}
void tidss_irq_disable_vblank(struct drm_crtc *crtc)
{
struct drm_device *ddev = crtc->dev;
struct tidss_device *tidss = ddev->dev_private;
struct tidss_crtc *tcrtc = to_tidss_crtc(crtc);
u32 hw_videoport = tcrtc->hw_videoport;
unsigned long flags;
spin_lock_irqsave(&tidss->wait_lock, flags);
tidss->irq_mask &= ~(DSS_IRQ_VP_VSYNC_EVEN(hw_videoport) |
DSS_IRQ_VP_VSYNC_ODD(hw_videoport));
tidss_irq_update(tidss);
spin_unlock_irqrestore(&tidss->wait_lock, flags);
}
irqreturn_t tidss_irq_handler(int irq, void *arg)
{
struct drm_device *ddev = (struct drm_device *)arg;
struct tidss_device *tidss = ddev->dev_private;
unsigned int id;
dispc_irq_t irqstatus;
if (WARN_ON(!ddev->irq_enabled))
return IRQ_NONE;
irqstatus = dispc_read_and_clear_irqstatus(tidss->dispc);
for (id = 0; id < tidss->num_crtcs; id++) {
struct drm_crtc *crtc = tidss->crtcs[id];
struct tidss_crtc *tcrtc = to_tidss_crtc(crtc);
u32 hw_videoport = tcrtc->hw_videoport;
if (irqstatus & (DSS_IRQ_VP_VSYNC_EVEN(hw_videoport) |
DSS_IRQ_VP_VSYNC_ODD(hw_videoport)))
tidss_crtc_vblank_irq(crtc);
if (irqstatus & (DSS_IRQ_VP_FRAME_DONE(hw_videoport)))
tidss_crtc_framedone_irq(crtc);
if (irqstatus & DSS_IRQ_VP_SYNC_LOST(hw_videoport))
tidss_crtc_error_irq(crtc, irqstatus);
}
if (irqstatus & DSS_IRQ_DEVICE_OCP_ERR)
dev_err_ratelimited(tidss->dev, "OCP error\n");
return IRQ_HANDLED;
}
void tidss_irq_resume(struct tidss_device *tidss)
{
unsigned long flags;
spin_lock_irqsave(&tidss->wait_lock, flags);
tidss_irq_update(tidss);
spin_unlock_irqrestore(&tidss->wait_lock, flags);
}
void tidss_irq_preinstall(struct drm_device *ddev)
{
struct tidss_device *tidss = ddev->dev_private;
spin_lock_init(&tidss->wait_lock);
tidss_runtime_get(tidss);
dispc_set_irqenable(tidss->dispc, 0);
dispc_read_and_clear_irqstatus(tidss->dispc);
tidss_runtime_put(tidss);
}
int tidss_irq_postinstall(struct drm_device *ddev)
{
struct tidss_device *tidss = ddev->dev_private;
unsigned long flags;
unsigned int i;
tidss_runtime_get(tidss);
spin_lock_irqsave(&tidss->wait_lock, flags);
tidss->irq_mask = DSS_IRQ_DEVICE_OCP_ERR;
for (i = 0; i < tidss->num_crtcs; ++i) {
struct tidss_crtc *tcrtc = to_tidss_crtc(tidss->crtcs[i]);
tidss->irq_mask |= DSS_IRQ_VP_SYNC_LOST(tcrtc->hw_videoport);
tidss->irq_mask |= DSS_IRQ_VP_FRAME_DONE(tcrtc->hw_videoport);
}
tidss_irq_update(tidss);
spin_unlock_irqrestore(&tidss->wait_lock, flags);
tidss_runtime_put(tidss);
return 0;
}
void tidss_irq_uninstall(struct drm_device *ddev)
{
struct tidss_device *tidss = ddev->dev_private;
tidss_runtime_get(tidss);
dispc_set_irqenable(tidss->dispc, 0);
tidss_runtime_put(tidss);
}
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (C) 2018 Texas Instruments Incorporated - http://www.ti.com/
* Author: Tomi Valkeinen <tomi.valkeinen@ti.com>
*/
#ifndef __TIDSS_IRQ_H__
#define __TIDSS_IRQ_H__
#include <linux/types.h>
#include "tidss_drv.h"
/*
* The IRQ status from various DISPC IRQ registers are packed into a single
* value, where the bits are defined as follows:
*
* bit group |dev|wb |mrg0|mrg1|mrg2|mrg3|plane0-3| <unused> |
* bit use |D |fou|FEOL|FEOL|FEOL|FEOL| UUUU | |
* bit number|0 |1-3|4-7 |8-11| 12-19 | 20-23 | 24-31 |
*
* device bits: D = OCP error
* WB bits: f = frame done wb, o = wb buffer overflow,
* u = wb buffer uncomplete
* vp bits: F = frame done, E = vsync even, O = vsync odd, L = sync lost
* plane bits: U = fifo underflow
*/
#define DSS_IRQ_DEVICE_OCP_ERR BIT(0)
#define DSS_IRQ_DEVICE_FRAMEDONEWB BIT(1)
#define DSS_IRQ_DEVICE_WBBUFFEROVERFLOW BIT(2)
#define DSS_IRQ_DEVICE_WBUNCOMPLETEERROR BIT(3)
#define DSS_IRQ_DEVICE_WB_MASK GENMASK(3, 1)
#define DSS_IRQ_VP_BIT_N(ch, bit) (4 + 4 * (ch) + (bit))
#define DSS_IRQ_PLANE_BIT_N(plane, bit) \
(DSS_IRQ_VP_BIT_N(TIDSS_MAX_PORTS, 0) + 1 * (plane) + (bit))
#define DSS_IRQ_VP_BIT(ch, bit) BIT(DSS_IRQ_VP_BIT_N((ch), (bit)))
#define DSS_IRQ_PLANE_BIT(plane, bit) \
BIT(DSS_IRQ_PLANE_BIT_N((plane), (bit)))
static inline dispc_irq_t DSS_IRQ_VP_MASK(u32 ch)
{
return GENMASK(DSS_IRQ_VP_BIT_N((ch), 3), DSS_IRQ_VP_BIT_N((ch), 0));
}
static inline dispc_irq_t DSS_IRQ_PLANE_MASK(u32 plane)
{
return GENMASK(DSS_IRQ_PLANE_BIT_N((plane), 0),
DSS_IRQ_PLANE_BIT_N((plane), 0));
}
#define DSS_IRQ_VP_FRAME_DONE(ch) DSS_IRQ_VP_BIT((ch), 0)
#define DSS_IRQ_VP_VSYNC_EVEN(ch) DSS_IRQ_VP_BIT((ch), 1)
#define DSS_IRQ_VP_VSYNC_ODD(ch) DSS_IRQ_VP_BIT((ch), 2)
#define DSS_IRQ_VP_SYNC_LOST(ch) DSS_IRQ_VP_BIT((ch), 3)
#define DSS_IRQ_PLANE_FIFO_UNDERFLOW(plane) DSS_IRQ_PLANE_BIT((plane), 0)
struct drm_crtc;
struct drm_device;
struct tidss_device;
void tidss_irq_enable_vblank(struct drm_crtc *crtc);
void tidss_irq_disable_vblank(struct drm_crtc *crtc);
void tidss_irq_preinstall(struct drm_device *ddev);
int tidss_irq_postinstall(struct drm_device *ddev);
void tidss_irq_uninstall(struct drm_device *ddev);
irqreturn_t tidss_irq_handler(int irq, void *arg);
void tidss_irq_resume(struct tidss_device *tidss);
#endif
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (C) 2018 Texas Instruments Incorporated - http://www.ti.com/
* Author: Tomi Valkeinen <tomi.valkeinen@ti.com>
*/
#include <drm/drm_atomic.h>
#include <drm/drm_atomic_helper.h>
#include <drm/drm_bridge.h>
#include <drm/drm_crtc_helper.h>
#include <drm/drm_fb_cma_helper.h>
#include <drm/drm_fb_helper.h>
#include <drm/drm_gem_framebuffer_helper.h>
#include <drm/drm_of.h>
#include <drm/drm_panel.h>
#include <drm/drm_vblank.h>
#include "tidss_crtc.h"
#include "tidss_dispc.h"
#include "tidss_drv.h"
#include "tidss_encoder.h"
#include "tidss_kms.h"
#include "tidss_plane.h"
static void tidss_atomic_commit_tail(struct drm_atomic_state *old_state)
{
struct drm_device *ddev = old_state->dev;
struct tidss_device *tidss = ddev->dev_private;
dev_dbg(ddev->dev, "%s\n", __func__);
tidss_runtime_get(tidss);
drm_atomic_helper_commit_modeset_disables(ddev, old_state);
drm_atomic_helper_commit_planes(ddev, old_state, 0);
drm_atomic_helper_commit_modeset_enables(ddev, old_state);
drm_atomic_helper_commit_hw_done(old_state);
drm_atomic_helper_wait_for_flip_done(ddev, old_state);
drm_atomic_helper_cleanup_planes(ddev, old_state);
tidss_runtime_put(tidss);
}
static const struct drm_mode_config_helper_funcs mode_config_helper_funcs = {
.atomic_commit_tail = tidss_atomic_commit_tail,
};
static const struct drm_mode_config_funcs mode_config_funcs = {
.fb_create = drm_gem_fb_create,
.atomic_check = drm_atomic_helper_check,
.atomic_commit = drm_atomic_helper_commit,
};
static int tidss_dispc_modeset_init(struct tidss_device *tidss)
{
struct device *dev = tidss->dev;
unsigned int fourccs_len;
const u32 *fourccs = dispc_plane_formats(tidss->dispc, &fourccs_len);
unsigned int i;
struct pipe {
u32 hw_videoport;
struct drm_bridge *bridge;
u32 enc_type;
};
const struct dispc_features *feat = tidss->feat;
u32 max_vps = feat->num_vps;
u32 max_planes = feat->num_planes;
struct pipe pipes[TIDSS_MAX_PORTS];
u32 num_pipes = 0;
u32 crtc_mask;
/* first find all the connected panels & bridges */
for (i = 0; i < max_vps; i++) {
struct drm_panel *panel;
struct drm_bridge *bridge;
u32 enc_type = DRM_MODE_ENCODER_NONE;
int ret;
ret = drm_of_find_panel_or_bridge(dev->of_node, i, 0,
&panel, &bridge);
if (ret == -ENODEV) {
dev_dbg(dev, "no panel/bridge for port %d\n", i);
continue;
} else if (ret) {
dev_dbg(dev, "port %d probe returned %d\n", i, ret);
return ret;
}
if (panel) {
u32 conn_type;
dev_dbg(dev, "Setting up panel for port %d\n", i);
switch (feat->vp_bus_type[i]) {
case DISPC_VP_OLDI:
enc_type = DRM_MODE_ENCODER_LVDS;
conn_type = DRM_MODE_CONNECTOR_LVDS;
break;
case DISPC_VP_DPI:
enc_type = DRM_MODE_ENCODER_DPI;
conn_type = DRM_MODE_CONNECTOR_LVDS;
break;
default:
WARN_ON(1);
return -EINVAL;
}
if (panel->connector_type != conn_type) {
dev_err(dev,
"%s: Panel %s has incompatible connector type for vp%d (%d != %d)\n",
__func__, dev_name(panel->dev), i,
panel->connector_type, conn_type);
return -EINVAL;
}
bridge = devm_drm_panel_bridge_add(dev, panel);
if (IS_ERR(bridge)) {
dev_err(dev,
"failed to set up panel bridge for port %d\n",
i);
return PTR_ERR(bridge);
}
}
pipes[num_pipes].hw_videoport = i;
pipes[num_pipes].bridge = bridge;
pipes[num_pipes].enc_type = enc_type;
num_pipes++;
}
/* all planes can be on any crtc */
crtc_mask = (1 << num_pipes) - 1;
/* then create a plane, a crtc and an encoder for each panel/bridge */
for (i = 0; i < num_pipes; ++i) {
struct tidss_plane *tplane;
struct tidss_crtc *tcrtc;
struct drm_encoder *enc;
u32 hw_plane_id = feat->vid_order[tidss->num_planes];
int ret;
tplane = tidss_plane_create(tidss, hw_plane_id,
DRM_PLANE_TYPE_PRIMARY, crtc_mask,
fourccs, fourccs_len);
if (IS_ERR(tplane)) {
dev_err(tidss->dev, "plane create failed\n");
return PTR_ERR(tplane);
}
tidss->planes[tidss->num_planes++] = &tplane->plane;
tcrtc = tidss_crtc_create(tidss, pipes[i].hw_videoport,
&tplane->plane);
if (IS_ERR(tcrtc)) {
dev_err(tidss->dev, "crtc create failed\n");
return PTR_ERR(tcrtc);
}
tidss->crtcs[tidss->num_crtcs++] = &tcrtc->crtc;
enc = tidss_encoder_create(tidss, pipes[i].enc_type,
1 << tcrtc->crtc.index);
if (IS_ERR(enc)) {
dev_err(tidss->dev, "encoder create failed\n");
return PTR_ERR(enc);
}
ret = drm_bridge_attach(enc, pipes[i].bridge, NULL);
if (ret) {
dev_err(tidss->dev, "bridge attach failed: %d\n", ret);
return ret;
}
}
/* create overlay planes of the leftover planes */
while (tidss->num_planes < max_planes) {
struct tidss_plane *tplane;
u32 hw_plane_id = feat->vid_order[tidss->num_planes];
tplane = tidss_plane_create(tidss, hw_plane_id,
DRM_PLANE_TYPE_OVERLAY, crtc_mask,
fourccs, fourccs_len);
if (IS_ERR(tplane)) {
dev_err(tidss->dev, "plane create failed\n");
return PTR_ERR(tplane);
}
tidss->planes[tidss->num_planes++] = &tplane->plane;
}
return 0;
}
int tidss_modeset_init(struct tidss_device *tidss)
{
struct drm_device *ddev = &tidss->ddev;
unsigned int i;
int ret;
dev_dbg(tidss->dev, "%s\n", __func__);
drm_mode_config_init(ddev);
ddev->mode_config.min_width = 8;
ddev->mode_config.min_height = 8;
ddev->mode_config.max_width = 8096;
ddev->mode_config.max_height = 8096;
ddev->mode_config.normalize_zpos = true;
ddev->mode_config.funcs = &mode_config_funcs;
ddev->mode_config.helper_private = &mode_config_helper_funcs;
ret = tidss_dispc_modeset_init(tidss);
if (ret)
goto err_mode_config_cleanup;
ret = drm_vblank_init(ddev, tidss->num_crtcs);
if (ret)
goto err_mode_config_cleanup;
/* Start with vertical blanking interrupt reporting disabled. */
for (i = 0; i < tidss->num_crtcs; ++i)
drm_crtc_vblank_reset(tidss->crtcs[i]);
drm_mode_config_reset(ddev);
dev_dbg(tidss->dev, "%s done\n", __func__);
return 0;
err_mode_config_cleanup:
drm_mode_config_cleanup(ddev);
return ret;
}
void tidss_modeset_cleanup(struct tidss_device *tidss)
{
struct drm_device *ddev = &tidss->ddev;
drm_mode_config_cleanup(ddev);
}
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (C) 2018 Texas Instruments Incorporated - http://www.ti.com/
* Author: Tomi Valkeinen <tomi.valkeinen@ti.com>
*/
#ifndef __TIDSS_KMS_H__
#define __TIDSS_KMS_H__
struct tidss_device;
int tidss_modeset_init(struct tidss_device *tidss);
void tidss_modeset_cleanup(struct tidss_device *tidss);
#endif
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (C) 2018 Texas Instruments Incorporated - http://www.ti.com/
* Author: Tomi Valkeinen <tomi.valkeinen@ti.com>
*/
#include <drm/drm_atomic.h>
#include <drm/drm_atomic_helper.h>
#include <drm/drm_crtc.h>
#include <drm/drm_crtc_helper.h>
#include <drm/drm_fourcc.h>
#include <drm/drm_fb_cma_helper.h>
#include "tidss_crtc.h"
#include "tidss_dispc.h"
#include "tidss_drv.h"
#include "tidss_plane.h"
/* drm_plane_helper_funcs */
static int tidss_plane_atomic_check(struct drm_plane *plane,
struct drm_plane_state *state)
{
struct drm_device *ddev = plane->dev;
struct tidss_device *tidss = ddev->dev_private;
struct tidss_plane *tplane = to_tidss_plane(plane);
const struct drm_format_info *finfo;
struct drm_crtc_state *crtc_state;
u32 hw_plane = tplane->hw_plane_id;
u32 hw_videoport;
int ret;
dev_dbg(ddev->dev, "%s\n", __func__);
if (!state->crtc) {
/*
* The visible field is not reset by the DRM core but only
* updated by drm_plane_helper_check_state(), set it manually.
*/
state->visible = false;
return 0;
}
crtc_state = drm_atomic_get_crtc_state(state->state, state->crtc);
if (IS_ERR(crtc_state))
return PTR_ERR(crtc_state);
ret = drm_atomic_helper_check_plane_state(state, crtc_state, 0,
INT_MAX, true, true);
if (ret < 0)
return ret;
/*
* The HW is only able to start drawing at subpixel boundary
* (the two first checks bellow). At the end of a row the HW
* can only jump integer number of subpixels forward to the
* beginning of the next row. So we can only show picture with
* integer subpixel width (the third check). However, after
* reaching the end of the drawn picture the drawing starts
* again at the absolute memory address where top left corner
* position of the drawn picture is (so there is no need to
* check for odd height).
*/
finfo = drm_format_info(state->fb->format->format);
if ((state->src_x >> 16) % finfo->hsub != 0) {
dev_dbg(ddev->dev,
"%s: x-position %u not divisible subpixel size %u\n",
__func__, (state->src_x >> 16), finfo->hsub);
return -EINVAL;
}
if ((state->src_y >> 16) % finfo->vsub != 0) {
dev_dbg(ddev->dev,
"%s: y-position %u not divisible subpixel size %u\n",
__func__, (state->src_y >> 16), finfo->vsub);
return -EINVAL;
}
if ((state->src_w >> 16) % finfo->hsub != 0) {
dev_dbg(ddev->dev,
"%s: src width %u not divisible by subpixel size %u\n",
__func__, (state->src_w >> 16), finfo->hsub);
return -EINVAL;
}
if (!state->visible)
return 0;
hw_videoport = to_tidss_crtc(state->crtc)->hw_videoport;
ret = dispc_plane_check(tidss->dispc, hw_plane, state, hw_videoport);
if (ret)
return ret;
return 0;
}
static void tidss_plane_atomic_update(struct drm_plane *plane,
struct drm_plane_state *old_state)
{
struct drm_device *ddev = plane->dev;
struct tidss_device *tidss = ddev->dev_private;
struct tidss_plane *tplane = to_tidss_plane(plane);
struct drm_plane_state *state = plane->state;
u32 hw_videoport;
int ret;
dev_dbg(ddev->dev, "%s\n", __func__);
if (!state->visible) {
dispc_plane_enable(tidss->dispc, tplane->hw_plane_id, false);
return;
}
hw_videoport = to_tidss_crtc(state->crtc)->hw_videoport;
ret = dispc_plane_setup(tidss->dispc, tplane->hw_plane_id,
state, hw_videoport);
if (ret) {
dev_err(plane->dev->dev, "%s: Failed to setup plane %d\n",
__func__, tplane->hw_plane_id);
dispc_plane_enable(tidss->dispc, tplane->hw_plane_id, false);
return;
}
dispc_plane_enable(tidss->dispc, tplane->hw_plane_id, true);
}
static void tidss_plane_atomic_disable(struct drm_plane *plane,
struct drm_plane_state *old_state)
{
struct drm_device *ddev = plane->dev;
struct tidss_device *tidss = ddev->dev_private;
struct tidss_plane *tplane = to_tidss_plane(plane);
dev_dbg(ddev->dev, "%s\n", __func__);
dispc_plane_enable(tidss->dispc, tplane->hw_plane_id, false);
}
static const struct drm_plane_helper_funcs tidss_plane_helper_funcs = {
.atomic_check = tidss_plane_atomic_check,
.atomic_update = tidss_plane_atomic_update,
.atomic_disable = tidss_plane_atomic_disable,
};
static const struct drm_plane_funcs tidss_plane_funcs = {
.update_plane = drm_atomic_helper_update_plane,
.disable_plane = drm_atomic_helper_disable_plane,
.reset = drm_atomic_helper_plane_reset,
.destroy = drm_plane_cleanup,
.atomic_duplicate_state = drm_atomic_helper_plane_duplicate_state,
.atomic_destroy_state = drm_atomic_helper_plane_destroy_state,
};
struct tidss_plane *tidss_plane_create(struct tidss_device *tidss,
u32 hw_plane_id, u32 plane_type,
u32 crtc_mask, const u32 *formats,
u32 num_formats)
{
struct tidss_plane *tplane;
enum drm_plane_type type;
u32 possible_crtcs;
u32 num_planes = tidss->feat->num_planes;
u32 color_encodings = (BIT(DRM_COLOR_YCBCR_BT601) |
BIT(DRM_COLOR_YCBCR_BT709));
u32 color_ranges = (BIT(DRM_COLOR_YCBCR_FULL_RANGE) |
BIT(DRM_COLOR_YCBCR_LIMITED_RANGE));
u32 default_encoding = DRM_COLOR_YCBCR_BT601;
u32 default_range = DRM_COLOR_YCBCR_FULL_RANGE;
u32 blend_modes = (BIT(DRM_MODE_BLEND_PREMULTI) |
BIT(DRM_MODE_BLEND_COVERAGE));
int ret;
tplane = devm_kzalloc(tidss->dev, sizeof(*tplane), GFP_KERNEL);
if (!tplane)
return ERR_PTR(-ENOMEM);
tplane->hw_plane_id = hw_plane_id;
possible_crtcs = crtc_mask;
type = plane_type;
ret = drm_universal_plane_init(&tidss->ddev, &tplane->plane,
possible_crtcs,
&tidss_plane_funcs,
formats, num_formats,
NULL, type, NULL);
if (ret < 0)
return ERR_PTR(ret);
drm_plane_helper_add(&tplane->plane, &tidss_plane_helper_funcs);
drm_plane_create_zpos_property(&tplane->plane, hw_plane_id, 0,
num_planes - 1);
ret = drm_plane_create_color_properties(&tplane->plane,
color_encodings,
color_ranges,
default_encoding,
default_range);
if (ret)
return ERR_PTR(ret);
ret = drm_plane_create_alpha_property(&tplane->plane);
if (ret)
return ERR_PTR(ret);
ret = drm_plane_create_blend_mode_property(&tplane->plane, blend_modes);
if (ret)
return ERR_PTR(ret);
return tplane;
}
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (C) 2018 Texas Instruments Incorporated - http://www.ti.com/
* Author: Tomi Valkeinen <tomi.valkeinen@ti.com>
*/
#ifndef __TIDSS_PLANE_H__
#define __TIDSS_PLANE_H__
#define to_tidss_plane(p) container_of((p), struct tidss_plane, plane)
struct tidss_device;
struct tidss_plane {
struct drm_plane plane;
u32 hw_plane_id;
};
struct tidss_plane *tidss_plane_create(struct tidss_device *tidss,
u32 hw_plane_id, u32 plane_type,
u32 crtc_mask, const u32 *formats,
u32 num_formats);
#endif
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (C) 2018 Texas Instruments Incorporated - http://www.ti.com/
* Author: Jyri Sarha <jsarha@ti.com>
*/
#include <linux/device.h>
#include <linux/kernel.h>
#include "tidss_scale_coefs.h"
/*
* These are interpolated with a custom python script from DSS5
* (drivers/gpu/drm/omapdrm/dss/dispc_coef.c) coefficients.
*/
static const struct tidss_scale_coefs coef5_m32 = {
.c2 = { 28, 34, 40, 46, 52, 58, 64, 70, 0, 2, 4, 8, 12, 16, 20, 24, },
.c1 = { 132, 138, 144, 150, 156, 162, 168, 174, 76, 84, 92, 98, 104, 110, 116, 124, },
.c0 = { 192, 192, 192, 190, 188, 186, 184, 182, 180, },
};
static const struct tidss_scale_coefs coef5_m26 = {
.c2 = { 24, 28, 32, 38, 44, 50, 56, 64, 0, 2, 4, 6, 8, 12, 16, 20, },
.c1 = { 132, 138, 144, 152, 160, 166, 172, 178, 72, 80, 88, 94, 100, 108, 116, 124, },
.c0 = { 200, 202, 204, 202, 200, 196, 192, 188, 184, },
};
static const struct tidss_scale_coefs coef5_m22 = {
.c2 = { 16, 20, 24, 30, 36, 42, 48, 56, 0, 0, 0, 2, 4, 8, 12, 14, },
.c1 = { 132, 140, 148, 156, 164, 172, 180, 186, 64, 72, 80, 88, 96, 104, 112, 122, },
.c0 = { 216, 216, 216, 214, 212, 208, 204, 198, 192, },
};
static const struct tidss_scale_coefs coef5_m19 = {
.c2 = { 12, 14, 16, 22, 28, 34, 40, 48, 0, 0, 0, 2, 4, 4, 4, 8, },
.c1 = { 128, 140, 152, 160, 168, 176, 184, 192, 56, 64, 72, 82, 92, 100, 108, 118, },
.c0 = { 232, 232, 232, 226, 220, 218, 216, 208, 200, },
};
static const struct tidss_scale_coefs coef5_m16 = {
.c2 = { 0, 2, 4, 8, 12, 18, 24, 32, 0, 0, 0, -2, -4, -4, -4, -2, },
.c1 = { 124, 138, 152, 164, 176, 186, 196, 206, 40, 48, 56, 68, 80, 90, 100, 112, },
.c0 = { 264, 262, 260, 254, 248, 242, 236, 226, 216, },
};
static const struct tidss_scale_coefs coef5_m14 = {
.c2 = { -8, -6, -4, -2, 0, 6, 12, 18, 0, -2, -4, -6, -8, -8, -8, -8, },
.c1 = { 120, 134, 148, 164, 180, 194, 208, 220, 24, 32, 40, 52, 64, 78, 92, 106, },
.c0 = { 288, 286, 284, 280, 276, 266, 256, 244, 232, },
};
static const struct tidss_scale_coefs coef5_m13 = {
.c2 = { -12, -12, -12, -10, -8, -4, 0, 6, 0, -2, -4, -6, -8, -10, -12, -12, },
.c1 = { 112, 130, 148, 164, 180, 196, 212, 228, 12, 22, 32, 44, 56, 70, 84, 98, },
.c0 = { 312, 308, 304, 298, 292, 282, 272, 258, 244, },
};
static const struct tidss_scale_coefs coef5_m12 = {
.c2 = { -16, -18, -20, -18, -16, -14, -12, -6, 0, -2, -4, -6, -8, -10, -12, -14, },
.c1 = { 104, 124, 144, 164, 184, 202, 220, 238, 0, 10, 20, 30, 40, 56, 72, 88, },
.c0 = { 336, 332, 328, 320, 312, 300, 288, 272, 256, },
};
static const struct tidss_scale_coefs coef5_m11 = {
.c2 = { -20, -22, -24, -24, -24, -24, -24, -20, 0, -2, -4, -6, -8, -10, -12, -16, },
.c1 = { 92, 114, 136, 158, 180, 204, 228, 250, -16, -8, 0, 12, 24, 38, 52, 72, },
.c0 = { 368, 364, 360, 350, 340, 326, 312, 292, 272, },
};
static const struct tidss_scale_coefs coef5_m10 = {
.c2 = { -16, -20, -24, -28, -32, -34, -36, -34, 0, 0, 0, -2, -4, -8, -12, -14, },
.c1 = { 72, 96, 120, 148, 176, 204, 232, 260, -32, -26, -20, -10, 0, 16, 32, 52, },
.c0 = { 400, 398, 396, 384, 372, 354, 336, 312, 288, },
};
static const struct tidss_scale_coefs coef5_m9 = {
.c2 = { -12, -18, -24, -28, -32, -38, -44, -46, 0, 2, 4, 2, 0, -2, -4, -8, },
.c1 = { 40, 68, 96, 128, 160, 196, 232, 268, -48, -46, -44, -36, -28, -14, 0, 20, },
.c0 = { 456, 450, 444, 428, 412, 388, 364, 334, 304, },
};
static const struct tidss_scale_coefs coef5_m8 = {
.c2 = { 0, -4, -8, -16, -24, -32, -40, -48, 0, 2, 4, 6, 8, 6, 4, 2, },
.c1 = { 0, 28, 56, 94, 132, 176, 220, 266, -56, -60, -64, -62, -60, -50, -40, -20, },
.c0 = { 512, 506, 500, 478, 456, 424, 392, 352, 312, },
};
static const struct tidss_scale_coefs coef3_m32 = {
.c1 = { 108, 92, 76, 62, 48, 36, 24, 140, 256, 236, 216, 198, 180, 162, 144, 126, },
.c0 = { 296, 294, 292, 288, 284, 278, 272, 136, 256, },
};
static const struct tidss_scale_coefs coef3_m26 = {
.c1 = { 104, 90, 76, 60, 44, 32, 20, 138, 256, 236, 216, 198, 180, 160, 140, 122, },
.c0 = { 304, 300, 296, 292, 288, 282, 276, 138, 256, },
};
static const struct tidss_scale_coefs coef3_m22 = {
.c1 = { 100, 84, 68, 54, 40, 30, 20, 138, 256, 236, 216, 196, 176, 156, 136, 118, },
.c0 = { 312, 310, 308, 302, 296, 286, 276, 138, 256, },
};
static const struct tidss_scale_coefs coef3_m19 = {
.c1 = { 96, 80, 64, 50, 36, 26, 16, 136, 256, 236, 216, 194, 172, 152, 132, 114, },
.c0 = { 320, 318, 316, 310, 304, 292, 280, 140, 256, },
};
static const struct tidss_scale_coefs coef3_m16 = {
.c1 = { 88, 72, 56, 44, 32, 22, 12, 134, 256, 234, 212, 190, 168, 148, 128, 108, },
.c0 = { 336, 332, 328, 320, 312, 300, 288, 144, 256, },
};
static const struct tidss_scale_coefs coef3_m14 = {
.c1 = { 80, 64, 48, 36, 24, 16, 8, 132, 256, 232, 208, 186, 164, 142, 120, 100, },
.c0 = { 352, 348, 344, 334, 324, 310, 296, 148, 256, },
};
static const struct tidss_scale_coefs coef3_m13 = {
.c1 = { 72, 56, 40, 30, 20, 12, 4, 130, 256, 232, 208, 184, 160, 136, 112, 92, },
.c0 = { 368, 364, 360, 346, 332, 316, 300, 150, 256, },
};
static const struct tidss_scale_coefs coef3_m12 = {
.c1 = { 64, 50, 36, 26, 16, 10, 4, 130, 256, 230, 204, 178, 152, 128, 104, 84, },
.c0 = { 384, 378, 372, 358, 344, 324, 304, 152, 256, },
};
static const struct tidss_scale_coefs coef3_m11 = {
.c1 = { 56, 40, 24, 16, 8, 4, 0, 128, 256, 228, 200, 172, 144, 120, 96, 76, },
.c0 = { 400, 396, 392, 376, 360, 336, 312, 156, 256, },
};
static const struct tidss_scale_coefs coef3_m10 = {
.c1 = { 40, 26, 12, 6, 0, -2, -4, 126, 256, 226, 196, 166, 136, 110, 84, 62, },
.c0 = { 432, 424, 416, 396, 376, 348, 320, 160, 256, },
};
static const struct tidss_scale_coefs coef3_m9 = {
.c1 = { 24, 12, 0, -4, -8, -8, -8, 124, 256, 222, 188, 154, 120, 92, 64, 44, },
.c0 = { 464, 456, 448, 424, 400, 366, 332, 166, 256, },
};
static const struct tidss_scale_coefs coef3_m8 = {
.c1 = { 0, -8, -16, -16, -16, -12, -8, 124, 256, 214, 172, 134, 96, 66, 36, 18, },
.c0 = { 512, 502, 492, 462, 432, 390, 348, 174, 256, },
};
const struct tidss_scale_coefs *tidss_get_scale_coefs(struct device *dev,
u32 firinc,
bool five_taps)
{
int i;
int inc;
static const struct {
int mmin;
int mmax;
const struct tidss_scale_coefs *coef3;
const struct tidss_scale_coefs *coef5;
const char *name;
} coefs[] = {
{ 27, 32, &coef3_m32, &coef5_m32, "M32" },
{ 23, 26, &coef3_m26, &coef5_m26, "M26" },
{ 20, 22, &coef3_m22, &coef5_m22, "M22" },
{ 17, 19, &coef3_m19, &coef5_m19, "M19" },
{ 15, 16, &coef3_m16, &coef5_m16, "M16" },
{ 14, 14, &coef3_m14, &coef5_m14, "M14" },
{ 13, 13, &coef3_m13, &coef5_m13, "M13" },
{ 12, 12, &coef3_m12, &coef5_m12, "M12" },
{ 11, 11, &coef3_m11, &coef5_m11, "M11" },
{ 10, 10, &coef3_m10, &coef5_m10, "M10" },
{ 9, 9, &coef3_m9, &coef5_m9, "M9" },
{ 4, 8, &coef3_m8, &coef5_m8, "M8" },
/*
* When upscaling more than two times, blockiness and outlines
* around the image are observed when M8 tables are used. M11,
* M16 and M19 tables are used to prevent this.
*/
{ 3, 3, &coef3_m11, &coef5_m11, "M11" },
{ 2, 2, &coef3_m16, &coef5_m16, "M16" },
{ 0, 1, &coef3_m19, &coef5_m19, "M19" },
};
/*
* inc is result of 0x200000 * in_size / out_size. This dividing
* by 0x40000 scales it down to 8 * in_size / out_size. After
* division the actual scaling factor is 8/inc.
*/
inc = firinc / 0x40000;
for (i = 0; i < ARRAY_SIZE(coefs); ++i) {
if (inc >= coefs[i].mmin && inc <= coefs[i].mmax) {
if (five_taps)
return coefs[i].coef5;
else
return coefs[i].coef3;
}
}
dev_err(dev, "%s: Coefficients not found for firinc 0x%08x, inc %d\n",
__func__, firinc, inc);
return NULL;
}
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (C) 2018 Texas Instruments Incorporated - http://www.ti.com/
* Author: Jyri Sarha <jsarha@ti.com>
*/
#ifndef __TIDSS_DISPC_COEF_H__
#define __TIDSS_DISPC_COEF_H__
#include <linux/types.h>
struct tidss_scale_coefs {
s16 c2[16];
s16 c1[16];
u16 c0[9];
};
const struct tidss_scale_coefs *tidss_get_scale_coefs(struct device *dev,
u32 firinc,
bool five_taps);
#endif
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment