Эх сурвалжийг харах

disp: msm: sde: add support for ppb size programming

MDSS 10.0 onwards, hw supports programming of pingpong
latency buffer size based on the resolution of display.
In prior targets full size of the latency buffer is used.
This change adds required support in sde driver to program
the pingpong buffer size based on systems recommended
latency lines requirement and the display resolution.

Change-Id: I172b19e5b397eb86190de57fed36f24cd67d2207
Signed-off-by: Prabhanjan Kandula <[email protected]>
Prabhanjan Kandula 2 жил өмнө
parent
commit
a2f3cba8ca

+ 18 - 0
msm/sde/sde_connector.h

@@ -1184,6 +1184,24 @@ int sde_connector_state_get_mode_info(struct drm_connector_state *conn_state,
 int sde_connector_get_lm_cnt_from_topology(struct drm_connector *conn,
 	 const struct drm_display_mode *drm_mode);
 
+/**
+ * sde_conn_get_max_mode_width - retrieves the maximum width from all modes
+ * conn: Pointer to DRM connector object
+ */
+static inline u32 sde_conn_get_max_mode_width(struct drm_connector *conn)
+{
+	u32 maxw = 0;
+	struct drm_display_mode *mode;
+
+	if (!conn)
+		return maxw;
+
+	list_for_each_entry(mode, &conn->modes, head)
+		maxw = maxw > mode->hdisplay ? maxw : mode->hdisplay;
+
+	return maxw;
+}
+
 /**
  * sde_connector_state_get_topology - get topology from given connector state
  * conn_state: Pointer to the DRM connector state object

+ 84 - 0
msm/sde/sde_encoder.c

@@ -1431,6 +1431,85 @@ static int _sde_encoder_update_roi(struct drm_encoder *drm_enc)
 	return 0;
 }
 
+static void _sde_encoder_update_ppb_size(struct drm_encoder *drm_enc)
+{
+	struct sde_kms *sde_kms;
+	struct sde_hw_mdp *hw_mdp;
+	struct drm_display_mode *mode;
+	struct sde_encoder_virt *sde_enc;
+	u32 maxw, pixels_per_pp, num_lm_or_pp, latency_lines;
+	int i;
+
+	if (!drm_enc) {
+		SDE_ERROR("invalid encoder parameter\n");
+		return;
+	}
+
+	sde_enc = to_sde_encoder_virt(drm_enc);
+	if (!sde_enc->cur_master || !sde_enc->cur_master->connector) {
+		SDE_ERROR_ENC(sde_enc, "invalid master or conn\n");
+		return;
+	}
+
+	/* program only for realtime displays */
+	if (sde_enc->disp_info.intf_type == DRM_MODE_CONNECTOR_VIRTUAL)
+		return;
+
+	sde_kms = sde_encoder_get_kms(&sde_enc->base);
+	if (!sde_kms) {
+		SDE_ERROR_ENC(sde_enc, "invalid sde_kms\n");
+		return;
+	}
+
+	/* check if hw support is available, early return if not available */
+	if (sde_kms->catalog->ppb_sz_program == SDE_PPB_SIZE_THRU_NONE)
+		return;
+
+	hw_mdp = sde_kms->hw_mdp;
+	if (!hw_mdp) {
+		SDE_ERROR_ENC(sde_enc, "invalid mdp top\n");
+		return;
+	}
+
+	mode = &drm_enc->crtc->state->adjusted_mode;
+	num_lm_or_pp = sde_enc->cur_channel_cnt;
+	latency_lines = sde_kms->catalog->ppb_buf_max_lines;
+
+	for (i = 0; i < num_lm_or_pp; i++) {
+		struct sde_hw_pingpong *hw_pp = sde_enc->hw_pp[i];
+		if (!hw_pp) {
+			SDE_ERROR_ENC(sde_enc, "invalid hw_pp i:%d pp_cnt:%d\n", i, num_lm_or_pp);
+			return;
+		}
+
+		if (hw_pp->ops.set_ppb_fifo_size) {
+			pixels_per_pp = mult_frac(mode->hdisplay, latency_lines, num_lm_or_pp);
+			hw_pp->ops.set_ppb_fifo_size(hw_pp, pixels_per_pp);
+
+			SDE_EVT32(DRMID(drm_enc), i, hw_pp->idx, mode->hdisplay, pixels_per_pp,
+					sde_kms->catalog->ppb_sz_program, SDE_EVTLOG_FUNC_CASE1);
+			SDE_DEBUG_ENC(sde_enc, "hw-pp i:%d pp_cnt:%d pixels_per_pp:%d\n",
+					i, num_lm_or_pp, pixels_per_pp);
+		} else if (hw_mdp->ops.set_ppb_fifo_size) {
+			maxw = sde_conn_get_max_mode_width(sde_enc->cur_master->connector);
+			if (!maxw) {
+				SDE_ERROR_ENC(sde_enc, "failed to get max horizantal resolution\n");
+				return;
+			}
+
+			pixels_per_pp = mult_frac(maxw, latency_lines, num_lm_or_pp);
+			hw_mdp->ops.set_ppb_fifo_size(hw_mdp, hw_pp->idx, pixels_per_pp);
+
+			SDE_EVT32(DRMID(drm_enc), i, hw_pp->idx, maxw, pixels_per_pp,
+					sde_kms->catalog->ppb_sz_program, SDE_EVTLOG_FUNC_CASE2);
+			SDE_DEBUG_ENC(sde_enc, "hw-pp i:%d pp_cnt:%d pixels_per_pp:%d\n",
+					i, num_lm_or_pp, pixels_per_pp);
+		} else {
+			SDE_ERROR_ENC(sde_enc, "invalid - ppb fifo size support is partial\n");
+		}
+	}
+}
+
 void sde_encoder_helper_vsync_config(struct sde_encoder_phys *phys_enc, u32 vsync_source)
 {
 	struct sde_vsync_source_cfg vsync_cfg = { 0 };
@@ -2549,12 +2628,14 @@ static void _sde_encoder_virt_populate_hw_res(struct drm_encoder *drm_enc)
 	struct sde_rm_hw_request request_hw;
 	int i, j;
 
+	sde_enc->cur_channel_cnt = 0;
 	sde_rm_init_hw_iter(&pp_iter, drm_enc->base.id, SDE_HW_BLK_PINGPONG);
 	for (i = 0; i < MAX_CHANNELS_PER_ENC; i++) {
 		sde_enc->hw_pp[i] = NULL;
 		if (!sde_rm_get_hw(&sde_kms->rm, &pp_iter))
 			break;
 		sde_enc->hw_pp[i] = to_sde_hw_pingpong(pp_iter.hw);
+		sde_enc->cur_channel_cnt++;
 	}
 
 	for (i = 0; i < sde_enc->num_phys_encs; i++) {
@@ -3000,6 +3081,9 @@ static void _sde_encoder_virt_enable_helper(struct drm_encoder *drm_enc)
 
 	_sde_encoder_update_vsync_source(sde_enc, &sde_enc->disp_info);
 
+	if (!sde_encoder_in_cont_splash(drm_enc))
+		_sde_encoder_update_ppb_size(drm_enc);
+
 	memset(&sde_enc->prv_conn_roi, 0, sizeof(sde_enc->prv_conn_roi));
 	memset(&sde_enc->cur_conn_roi, 0, sizeof(sde_enc->cur_conn_roi));
 	_sde_encoder_control_fal10_veto(drm_enc, true);

+ 2 - 0
msm/sde/sde_encoder.h

@@ -158,6 +158,7 @@ enum sde_sim_qsync_event {
  *			pingpong blocks can be different than num_phys_encs.
  * @hw_dsc:		Array of DSC block handles used for the display.
  * @hw_vdc:		Array of VDC block handles used for the display.
+ * @cur_channel_cnt     Number of data channels currently used for the display
  * @dirty_dsc_ids:	Cached dsc indexes for dirty DSC blocks needing flush
  * @intfs_swapped	Whether or not the phys_enc interfaces have been swapped
  *			for partial update right-only cases, such as pingpong
@@ -243,6 +244,7 @@ struct sde_encoder_virt {
 	struct sde_hw_pingpong *hw_dsc_pp[MAX_CHANNELS_PER_ENC];
 	enum sde_dsc dirty_dsc_ids[MAX_CHANNELS_PER_ENC];
 	enum sde_vdc dirty_vdc_ids[MAX_CHANNELS_PER_ENC];
+	u32 cur_channel_cnt;
 	bool intfs_swapped;
 	bool qdss_status;
 

+ 12 - 0
msm/sde/sde_hw_catalog.c

@@ -147,6 +147,7 @@
 #define DEFAULT_AXI_BUS_WIDTH			32
 #define DEFAULT_CPU_MASK			0
 #define DEFAULT_CPU_DMA_LATENCY			PM_QOS_DEFAULT_VALUE
+#define DEFAULT_PPB_BUF_MAX_LINES		4
 
 /* Uidle values */
 #define SDE_UIDLE_FAL10_EXIT_CNT 128
@@ -4050,6 +4051,9 @@ static int sde_pp_parse_dt(struct device_node *np, struct sde_mdss_cfg *sde_cfg)
 			}
 		}
 
+		if (sde_cfg->ppb_sz_program == SDE_PPB_SIZE_THRU_PINGPONG)
+			set_bit(SDE_PINGPONG_SET_SIZE, &pp->features);
+
 		sblk->dither.base = PROP_VALUE_ACCESS(prop_value, DITHER_OFF,
 				i);
 		if (sblk->dither.base) {
@@ -4243,6 +4247,9 @@ static int sde_top_parse_dt(struct device_node *np, struct sde_mdss_cfg *cfg)
 	else if (major_version < SDE_HW_MAJOR(SDE_HW_VER_810))
 		set_bit(SDE_MDP_WD_TIMER, &cfg->mdp[0].features);
 
+	if (cfg->ppb_sz_program == SDE_PPB_SIZE_THRU_TOP)
+		set_bit(SDE_MDP_TOP_PPB_SET_SIZE, &cfg->mdp[0].features);
+
 	rc = _add_to_irq_offset_list(cfg, SDE_INTR_HWBLK_TOP,
 			SDE_INTR_TOP_INTR, cfg->mdp[0].base);
 	if (rc)
@@ -5017,6 +5024,9 @@ static int _sde_hardware_pre_caps(struct sde_mdss_cfg *sde_cfg, uint32_t hw_rev)
 	set_bit(SDE_FEATURE_HDR, sde_cfg->features);
 	sde_cfg->mdss_hw_block_size = DEFAULT_MDSS_HW_BLOCK_SIZE;
 
+	/* Set target specific value based on sytems recommendation if not same as default value */
+	sde_cfg->ppb_buf_max_lines = DEFAULT_PPB_BUF_MAX_LINES;
+
 	for (i = 0; i < SSPP_MAX; i++) {
 		sde_cfg->demura_supported[i][0] = ~0x0;
 		sde_cfg->demura_supported[i][1] = ~0x0;
@@ -5399,6 +5409,8 @@ static int _sde_hardware_pre_caps(struct sde_mdss_cfg *sde_cfg, uint32_t hw_rev)
 		set_bit(SDE_FEATURE_EPT_FPS, sde_cfg->features);
 		sde_cfg->allowed_dsc_reservation_switch = SDE_DP_DSC_RESERVATION_SWITCH;
 		sde_cfg->autorefresh_disable_seq = AUTOREFRESH_DISABLE_SEQ2;
+		/* if pingpong block supports it this should not be set on top block */
+		sde_cfg->ppb_sz_program = SDE_PPB_SIZE_THRU_TOP;
 		sde_cfg->perf.min_prefill_lines = 40;
 		sde_cfg->vbif_qos_nlvl = 8;
 		sde_cfg->qos_target_time_ns = 11160;

+ 24 - 2
msm/sde/sde_hw_catalog.h

@@ -151,6 +151,8 @@
 
 #define DNSC_BLUR_MAX_RATIO_COUNT	7
 
+#define MDP_PPB_FIFO_ENTRY_SIZE                4
+
 /*
  * UIDLE supported versions
  */
@@ -267,8 +269,8 @@ struct sde_intr_irq_offsets {
  * @SDE_MDP_DHDR_MEMPOOL   Dynamic HDR Metadata mempool present
  * @SDE_MDP_DHDR_MEMPOOL_4K Dynamic HDR mempool is 4k aligned
  * @SDE_MDP_PERIPH_TOP_REMOVED Indicates if periph top0 block is removed
- * @SDE_MDP_MAX            Maximum value
-
+ * @SDE_MDP_TOP_PPB_SET_SIZE   Indicates if top block supports ppb size setting
+ * @SDE_MDP_MAX                Maximum value
  */
 enum {
 	SDE_MDP_PANIC_PER_PIPE = 0x1,
@@ -281,6 +283,7 @@ enum {
 	SDE_MDP_DHDR_MEMPOOL,
 	SDE_MDP_DHDR_MEMPOOL_4K,
 	SDE_MDP_PERIPH_TOP_0_REMOVED,
+	SDE_MDP_TOP_PPB_SET_SIZE,
 	SDE_MDP_MAX
 };
 
@@ -512,6 +515,7 @@ enum {
  * @SDE_PINGPONG_MERGE_3D,  Separate MERGE_3D block exists
  * @SDE_PINGPONG_CWB,           PP block supports CWB
  * @SDE_PINGPONG_CWB_DITHER,    PP block supports CWB dither
+ * @SDE_PINGPONG_SET_SIZE,      PP block supports setting latency buffer size
  * @SDE_PINGPONG_MAX
  */
 enum {
@@ -525,6 +529,7 @@ enum {
 	SDE_PINGPONG_MERGE_3D,
 	SDE_PINGPONG_CWB,
 	SDE_PINGPONG_CWB_DITHER,
+	SDE_PINGPONG_SET_SIZE,
 	SDE_PINGPONG_MAX
 };
 
@@ -731,6 +736,18 @@ enum {
 	SDE_UIDLE_MAX
 };
 
+/**
+ * sde_ppb_size_option           PPB size limit programming choice
+ * @SDE_PPB_SIZE_THRU_NONE       ppb size programming not available
+ * @SDE_PPB_SIZE_THRU_TOP        ppb size programming supported in top block
+ * @SDE_PPB_SIZE_THRU_PINGPONG   ppb size programming supported in pingpong block
+ */
+enum sde_ppb_size_option {
+	SDE_PPB_SIZE_THRU_NONE,
+	SDE_PPB_SIZE_THRU_TOP,
+	SDE_PPB_SIZE_THRU_PINGPONG,
+};
+
 /**
  * MDSS features - For enabling target specific functionality in @sde_mdss_cfg "features" bitmap
  * @SDE_FEATURE_CDP            Client driven prefetch supported
@@ -1941,6 +1958,8 @@ struct sde_perf_cfg {
  * @dnsc_blur_filter_count   supported filter count for downscale blur
  * @ipcc_protocol_id    ipcc protocol id for the hw
  * @ipcc_client_phys_id dpu ipcc client id for the hw, physical client id if supported
+ * @ppb_sz_program      enum value for pingpong buffer size programming choice by hw
+ * @ppb_buf_max_lines   maximum lines needed for pingpong latency buffer size
  */
 struct sde_mdss_cfg {
 	/* Block Revisions */
@@ -2062,6 +2081,9 @@ struct sde_mdss_cfg {
 
 	u32 ipcc_protocol_id;
 	u32 ipcc_client_phys_id;
+
+	enum sde_ppb_size_option ppb_sz_program;
+	u32 ppb_buf_max_lines;
 };
 
 struct sde_mdss_hw_cfg_handler {

+ 24 - 1
msm/sde/sde_hw_pingpong.c

@@ -1,6 +1,6 @@
 // SPDX-License-Identifier: GPL-2.0-only
 /*
- * Copyright (c) 2021-2022 Qualcomm Innovation Center, Inc. All rights reserved.
+ * Copyright (c) 2021-2023 Qualcomm Innovation Center, Inc. All rights reserved.
  * Copyright (c) 2015-2021, The Linux Foundation. All rights reserved.
  */
 
@@ -42,6 +42,9 @@
 #define MERGE_3D_MODE 0x004
 #define MERGE_3D_MUX  0x000
 
+#define PPB_FIFO_SIZE_CFG               0x01C
+#define PPB_FIFO_SIZE_MASK              0x0FFF
+
 static struct sde_merge_3d_cfg *_merge_3d_offset(enum sde_merge_3d idx,
 		struct sde_mdss_cfg *m,
 		void __iomem *addr,
@@ -439,6 +442,22 @@ line_count_exit:
 	return line;
 }
 
+static void sde_hw_pp_set_ppb_fifo_size(struct sde_hw_pingpong *pp, u32 pixels)
+{
+	struct sde_hw_blk_reg_map *c;
+	u32 val;
+
+	if (!pp)
+		return;
+
+	c = &pp->hw;
+
+	/* covert to fifo units, 4 pixels can be stored per fifo */
+	val = (pixels / MDP_PPB_FIFO_ENTRY_SIZE) & 0x0FFF;
+
+	SDE_REG_WRITE(c, PPB_FIFO_SIZE_CFG, val);
+}
+
 static void sde_hw_pp_setup_3d_merge_mode(struct sde_hw_pingpong *pp,
 					enum sde_3d_blend_mode cfg)
 {
@@ -473,7 +492,11 @@ static void _setup_pingpong_ops(struct sde_hw_pingpong_ops *ops,
 		ops->get_autorefresh = sde_hw_pp_get_autorefresh_config;
 		ops->poll_timeout_wr_ptr = sde_hw_pp_poll_timeout_wr_ptr;
 		ops->get_line_count = sde_hw_pp_get_line_count;
+	} else if (hw_cap->features & BIT(SDE_PINGPONG_SET_SIZE)) {
+		/* PPB_FIFO_CFG offset conflicts with legacy PP Tear registers */
+		ops->set_ppb_fifo_size = sde_hw_pp_set_ppb_fifo_size;
 	}
+
 	if (hw_cap->features & BIT(SDE_PINGPONG_DSC)) {
 		ops->setup_dsc = sde_hw_pp_setup_dsc;
 		ops->enable_dsc = sde_hw_pp_dsc_enable;

+ 6 - 1
msm/sde/sde_hw_pingpong.h

@@ -1,6 +1,6 @@
 /* SPDX-License-Identifier: GPL-2.0-only */
 /*
- * Copyright (c) 2022 Qualcomm Innovation Center, Inc. All rights reserved.
+ * Copyright (c) 2022-2023 Qualcomm Innovation Center, Inc. All rights reserved.
  * Copyright (c) 2015-2021, The Linux Foundation. All rights reserved.
  */
 
@@ -131,6 +131,11 @@ struct sde_hw_pingpong_ops {
 	 * get PP features supported by this instance
 	 */
 	unsigned long (*get_hw_caps)(struct sde_hw_pingpong *pp);
+
+	/**
+	 * set_ppb_fifo_size - set ppb latency buffer size to a fixed value
+	 */
+	void (*set_ppb_fifo_size)(struct sde_hw_pingpong *pp, u32 pixels);
 };
 
 struct sde_hw_merge_3d_ops {

+ 40 - 1
msm/sde/sde_hw_top.c

@@ -1,6 +1,6 @@
 // SPDX-License-Identifier: GPL-2.0-only
 /*
- * Copyright (c) 2021-2022 Qualcomm Innovation Center, Inc. All rights reserved.
+ * Copyright (c) 2021-2023 Qualcomm Innovation Center, Inc. All rights reserved.
  * Copyright (c) 2015-2021, The Linux Foundation. All rights reserved.
  */
 
@@ -118,6 +118,8 @@
 #define HW_FENCE_IPCC_FENCE_PROTOCOL_ID 4
 #define HW_FENCE_DPU_FENCE_PROTOCOL_ID 3
 
+static int ppb_offset_map[PINGPONG_MAX] = {1, 0, 3, 2, 5, 4, 7, 7, 6, 6, -1, -1};
+
 static void sde_hw_setup_split_pipe(struct sde_hw_mdp *mdp,
 		struct split_pipe_cfg *cfg)
 {
@@ -767,6 +769,38 @@ static void sde_hw_setup_hw_fences_config(struct sde_hw_mdp *mdp, u32 protocol_i
 	SDE_REG_WRITE(&c, offset, val);
 }
 
+void sde_hw_top_set_ppb_fifo_size(struct sde_hw_mdp *mdp, u32 pp, u32 sz)
+{
+	struct sde_hw_blk_reg_map c;
+	u32 offset, val, pp_index;
+
+	if (!mdp) {
+		SDE_ERROR("invalid mdp instance\n");
+		return;
+	}
+
+	if (pp >= PINGPONG_MAX || ppb_offset_map[pp - PINGPONG_0] < 0) {
+		SDE_ERROR("invalid pingpong index:%d max:%d\n", pp, PINGPONG_MAX);
+		return;
+	}
+
+	pp_index = pp - PINGPONG_0;
+
+	c = mdp->hw;
+	offset = PPB_FIFO_SIZE + ((ppb_offset_map[pp_index] / 2) * 0x4);
+
+	spin_lock(&mdp->slock);
+	/* read, modify & update *respective 16 bit fields */
+	val = SDE_REG_READ(&c, offset);
+
+	/* divide by 4 as each fifo entry can store 4 pixels */
+	sz = (sz / MDP_PPB_FIFO_ENTRY_SIZE) & 0xFFFF;
+	sz = ppb_offset_map[pp_index] % 2 ? (sz << 16) : sz;
+	val = (ppb_offset_map[pp_index] % 2) ? (val & 0xFFFF) : (val & 0xFFFF0000);
+	SDE_REG_WRITE(&c, offset, val | sz);
+	spin_unlock(&mdp->slock);
+}
+
 static void _setup_mdp_ops(struct sde_hw_mdp_ops *ops, unsigned long cap, u32 hw_fence_rev)
 {
 	ops->setup_split_pipe = sde_hw_setup_split_pipe;
@@ -793,6 +827,9 @@ static void _setup_mdp_ops(struct sde_hw_mdp_ops *ops, unsigned long cap, u32 hw
 		ops->hw_fence_input_timestamp_ctrl = sde_hw_hw_fence_timestamp_ctrl;
 		ops->hw_fence_input_status = sde_hw_input_hw_fence_status;
 	}
+
+	if (cap & BIT(SDE_MDP_TOP_PPB_SET_SIZE))
+		ops->set_ppb_fifo_size = sde_hw_top_set_ppb_fifo_size;
 }
 
 static const struct sde_mdp_cfg *_top_offset(enum sde_mdp mdp,
@@ -839,6 +876,8 @@ struct sde_hw_mdp *sde_hw_mdptop_init(enum sde_mdp idx,
 		return ERR_PTR(-EINVAL);
 	}
 
+	spin_lock_init(&mdp->slock);
+
 	/*
 	 * Assign ops
 	 */

+ 11 - 1
msm/sde/sde_hw_top.h

@@ -1,6 +1,6 @@
 /* SPDX-License-Identifier: GPL-2.0-only */
 /*
- * Copyright (c) 2022 Qualcomm Innovation Center, Inc. All rights reserved.
+ * Copyright (c) 2022-2023 Qualcomm Innovation Center, Inc. All rights reserved.
  * Copyright (c) 2015-2021, The Linux Foundation. All rights reserved.
  */
 
@@ -230,6 +230,14 @@ struct sde_hw_mdp_ops {
 	 * @enable:    indicates if timestamps should be cleared
 	 */
 	void (*hw_fence_input_timestamp_ctrl)(struct sde_hw_mdp *mdp, bool enable, bool clear);
+
+	/**
+	 * set_ppb_fifo_size - set ppb latency buffer size to a fixed value
+	 * @mdp:      mdp top context driver
+	 * @pp:       indicates pingpong block id
+	 * @sz:       indicates size of the ppb in terms of pixels
+	 */
+	void (*set_ppb_fifo_size)(struct sde_hw_mdp *mdp, u32 pp, u32 sz);
 };
 
 struct sde_hw_mdp {
@@ -239,6 +247,8 @@ struct sde_hw_mdp {
 	enum sde_mdp idx;
 	const struct sde_mdp_cfg *caps;
 
+	spinlock_t slock;
+
 	/* ops */
 	struct sde_hw_mdp_ops ops;
 };

+ 2 - 0
msm/sde/sde_hwio.h

@@ -1,6 +1,7 @@
 /* SPDX-License-Identifier: GPL-2.0-only */
 /*
  * Copyright (c) 2015-2020, The Linux Foundation. All rights reserved.
+ * Copyright (c) 2023 Qualcomm Innovation Center, Inc. All rights reserved.
  */
 
 #ifndef _SDE_HWIO_H
@@ -33,6 +34,7 @@
 #define PPB0_CONFIG                     0x334
 #define PPB1_CNTL                       0x338
 #define PPB1_CONFIG                     0x33C
+#define PPB_FIFO_SIZE                   0x350
 #define PPB2_CNTL                       0x370
 #define PPB3_CNTL                       0x374
 #define HW_EVENTS_CTL                   0x37C