diff options
Diffstat (limited to 'drivers/gpu/drm/tilcdc/tilcdc_crtc.c')
| -rw-r--r-- | drivers/gpu/drm/tilcdc/tilcdc_crtc.c | 602 | 
1 files changed, 602 insertions, 0 deletions
| diff --git a/drivers/gpu/drm/tilcdc/tilcdc_crtc.c b/drivers/gpu/drm/tilcdc/tilcdc_crtc.c new file mode 100644 index 000000000000..5dd3c7d031d5 --- /dev/null +++ b/drivers/gpu/drm/tilcdc/tilcdc_crtc.c @@ -0,0 +1,602 @@ +/* + * Copyright (C) 2012 Texas Instruments + * Author: Rob Clark <[email protected]> + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published by + * the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program.  If not, see <http://www.gnu.org/licenses/>. + */ + +#include <linux/kfifo.h> + +#include "tilcdc_drv.h" +#include "tilcdc_regs.h" + +struct tilcdc_crtc { +	struct drm_crtc base; + +	const struct tilcdc_panel_info *info; +	uint32_t dirty; +	dma_addr_t start, end; +	struct drm_pending_vblank_event *event; +	int dpms; +	wait_queue_head_t frame_done_wq; +	bool frame_done; + +	/* fb currently set to scanout 0/1: */ +	struct drm_framebuffer *scanout[2]; + +	/* for deferred fb unref's: */ +	DECLARE_KFIFO_PTR(unref_fifo, struct drm_framebuffer *); +	struct work_struct work; +}; +#define to_tilcdc_crtc(x) container_of(x, struct tilcdc_crtc, base) + +static void unref_worker(struct work_struct *work) +{ +	struct tilcdc_crtc *tilcdc_crtc = container_of(work, struct tilcdc_crtc, work); +	struct drm_device *dev = tilcdc_crtc->base.dev; +	struct drm_framebuffer *fb; + +	mutex_lock(&dev->mode_config.mutex); +	while (kfifo_get(&tilcdc_crtc->unref_fifo, &fb)) +		drm_framebuffer_unreference(fb); +	mutex_unlock(&dev->mode_config.mutex); +} + +static void set_scanout(struct drm_crtc *crtc, int n) +{ +	static const uint32_t base_reg[] = { +			LCDC_DMA_FB_BASE_ADDR_0_REG, LCDC_DMA_FB_BASE_ADDR_1_REG, +	}; +	static const uint32_t ceil_reg[] = { +			LCDC_DMA_FB_CEILING_ADDR_0_REG, LCDC_DMA_FB_CEILING_ADDR_1_REG, +	}; +	static const uint32_t stat[] = { +			LCDC_END_OF_FRAME0, LCDC_END_OF_FRAME1, +	}; +	struct tilcdc_crtc *tilcdc_crtc = to_tilcdc_crtc(crtc); +	struct drm_device *dev = crtc->dev; + +	pm_runtime_get_sync(dev->dev); +	tilcdc_write(dev, base_reg[n], tilcdc_crtc->start); +	tilcdc_write(dev, ceil_reg[n], tilcdc_crtc->end); +	if (tilcdc_crtc->scanout[n]) { +		if (kfifo_put(&tilcdc_crtc->unref_fifo, +				(const struct drm_framebuffer **)&tilcdc_crtc->scanout[n])) { +			struct tilcdc_drm_private *priv = dev->dev_private; +			queue_work(priv->wq, &tilcdc_crtc->work); +		} else { +			dev_err(dev->dev, "unref fifo full!\n"); +			drm_framebuffer_unreference(tilcdc_crtc->scanout[n]); +		} +	} +	tilcdc_crtc->scanout[n] = crtc->fb; +	drm_framebuffer_reference(tilcdc_crtc->scanout[n]); +	tilcdc_crtc->dirty &= ~stat[n]; +	pm_runtime_put_sync(dev->dev); +} + +static void update_scanout(struct drm_crtc *crtc) +{ +	struct tilcdc_crtc *tilcdc_crtc = to_tilcdc_crtc(crtc); +	struct drm_device *dev = crtc->dev; +	struct drm_framebuffer *fb = crtc->fb; +	struct drm_gem_cma_object *gem; +	unsigned int depth, bpp; + +	drm_fb_get_bpp_depth(fb->pixel_format, &depth, &bpp); +	gem = drm_fb_cma_get_gem_obj(fb, 0); + +	tilcdc_crtc->start = gem->paddr + fb->offsets[0] + +			(crtc->y * fb->pitches[0]) + (crtc->x * bpp/8); + +	tilcdc_crtc->end = tilcdc_crtc->start + +			(crtc->mode.vdisplay * fb->pitches[0]); + +	if (tilcdc_crtc->dpms == DRM_MODE_DPMS_ON) { +		/* already enabled, so just mark the frames that need +		 * updating and they will be updated on vblank: +		 */ +		tilcdc_crtc->dirty |= LCDC_END_OF_FRAME0 | LCDC_END_OF_FRAME1; +		drm_vblank_get(dev, 0); +	} else { +		/* not enabled yet, so update registers immediately: */ +		set_scanout(crtc, 0); +		set_scanout(crtc, 1); +	} +} + +static void start(struct drm_crtc *crtc) +{ +	struct drm_device *dev = crtc->dev; +	struct tilcdc_drm_private *priv = dev->dev_private; + +	if (priv->rev == 2) { +		tilcdc_set(dev, LCDC_CLK_RESET_REG, LCDC_CLK_MAIN_RESET); +		msleep(1); +		tilcdc_clear(dev, LCDC_CLK_RESET_REG, LCDC_CLK_MAIN_RESET); +		msleep(1); +	} + +	tilcdc_set(dev, LCDC_DMA_CTRL_REG, LCDC_DUAL_FRAME_BUFFER_ENABLE); +	tilcdc_set(dev, LCDC_RASTER_CTRL_REG, LCDC_PALETTE_LOAD_MODE(DATA_ONLY)); +	tilcdc_set(dev, LCDC_RASTER_CTRL_REG, LCDC_RASTER_ENABLE); +} + +static void stop(struct drm_crtc *crtc) +{ +	struct drm_device *dev = crtc->dev; + +	tilcdc_clear(dev, LCDC_RASTER_CTRL_REG, LCDC_RASTER_ENABLE); +} + +static void tilcdc_crtc_destroy(struct drm_crtc *crtc) +{ +	struct tilcdc_crtc *tilcdc_crtc = to_tilcdc_crtc(crtc); + +	WARN_ON(tilcdc_crtc->dpms == DRM_MODE_DPMS_ON); + +	drm_crtc_cleanup(crtc); +	WARN_ON(!kfifo_is_empty(&tilcdc_crtc->unref_fifo)); +	kfifo_free(&tilcdc_crtc->unref_fifo); +	kfree(tilcdc_crtc); +} + +static int tilcdc_crtc_page_flip(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) { +		dev_err(dev->dev, "already pending page flip!\n"); +		return -EBUSY; +	} + +	crtc->fb = fb; +	tilcdc_crtc->event = event; +	update_scanout(crtc); + +	return 0; +} + +static void tilcdc_crtc_dpms(struct drm_crtc *crtc, int mode) +{ +	struct tilcdc_crtc *tilcdc_crtc = to_tilcdc_crtc(crtc); +	struct drm_device *dev = crtc->dev; +	struct tilcdc_drm_private *priv = dev->dev_private; + +	/* we really only care about on or off: */ +	if (mode != DRM_MODE_DPMS_ON) +		mode = DRM_MODE_DPMS_OFF; + +	if (tilcdc_crtc->dpms == mode) +		return; + +	tilcdc_crtc->dpms = mode; + +	pm_runtime_get_sync(dev->dev); + +	if (mode == DRM_MODE_DPMS_ON) { +		pm_runtime_forbid(dev->dev); +		start(crtc); +	} else { +		tilcdc_crtc->frame_done = false; +		stop(crtc); + +		/* if necessary wait for framedone irq which will still come +		 * before putting things to sleep.. +		 */ +		if (priv->rev == 2) { +			int ret = wait_event_timeout( +					tilcdc_crtc->frame_done_wq, +					tilcdc_crtc->frame_done, +					msecs_to_jiffies(50)); +			if (ret == 0) +				dev_err(dev->dev, "timeout waiting for framedone\n"); +		} +		pm_runtime_allow(dev->dev); +	} + +	pm_runtime_put_sync(dev->dev); +} + +static bool tilcdc_crtc_mode_fixup(struct drm_crtc *crtc, +		const struct drm_display_mode *mode, +		struct drm_display_mode *adjusted_mode) +{ +	return true; +} + +static void tilcdc_crtc_prepare(struct drm_crtc *crtc) +{ +	tilcdc_crtc_dpms(crtc, DRM_MODE_DPMS_OFF); +} + +static void tilcdc_crtc_commit(struct drm_crtc *crtc) +{ +	tilcdc_crtc_dpms(crtc, DRM_MODE_DPMS_ON); +} + +static int tilcdc_crtc_mode_set(struct drm_crtc *crtc, +		struct drm_display_mode *mode, +		struct drm_display_mode *adjusted_mode, +		int x, int y, +		struct drm_framebuffer *old_fb) +{ +	struct tilcdc_crtc *tilcdc_crtc = to_tilcdc_crtc(crtc); +	struct drm_device *dev = crtc->dev; +	struct tilcdc_drm_private *priv = dev->dev_private; +	const struct tilcdc_panel_info *info = tilcdc_crtc->info; +	uint32_t reg, hbp, hfp, hsw, vbp, vfp, vsw; +	int ret; + +	ret = tilcdc_crtc_mode_valid(crtc, mode); +	if (WARN_ON(ret)) +		return ret; + +	if (WARN_ON(!info)) +		return -EINVAL; + +	pm_runtime_get_sync(dev->dev); + +	/* Configure the Burst Size and fifo threshold of DMA: */ +	reg = tilcdc_read(dev, LCDC_DMA_CTRL_REG) & ~0x00000770; +	switch (info->dma_burst_sz) { +	case 1: +		reg |= LCDC_DMA_BURST_SIZE(LCDC_DMA_BURST_1); +		break; +	case 2: +		reg |= LCDC_DMA_BURST_SIZE(LCDC_DMA_BURST_2); +		break; +	case 4: +		reg |= LCDC_DMA_BURST_SIZE(LCDC_DMA_BURST_4); +		break; +	case 8: +		reg |= LCDC_DMA_BURST_SIZE(LCDC_DMA_BURST_8); +		break; +	case 16: +		reg |= LCDC_DMA_BURST_SIZE(LCDC_DMA_BURST_16); +		break; +	default: +		return -EINVAL; +	} +	reg |= (info->fifo_th << 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); + +	/* Configure the AC Bias Period and Number of Transitions per Interrupt: */ +	reg = tilcdc_read(dev, LCDC_RASTER_TIMING_2_REG) & ~0x000fff00; +	reg |= LCDC_AC_BIAS_FREQUENCY(info->ac_bias) | +		LCDC_AC_BIAS_TRANSITIONS_PER_INT(info->ac_bias_intrpt); +	if (priv->rev == 2) { +		reg |= (hfp & 0x300) >> 8; +		reg |= (hbp & 0x300) >> 4; +		reg |= (hsw & 0x3c0) << 21; +	} +	tilcdc_write(dev, LCDC_RASTER_TIMING_2_REG, reg); + +	reg = (((mode->hdisplay >> 4) - 1) << 4) | +		((hbp & 0xff) << 24) | +		((hfp & 0xff) << 16) | +		((hsw & 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 & 0x3f) << 10); +	tilcdc_write(dev, LCDC_RASTER_TIMING_1_REG, reg); + +	/* 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); +	reg |= LCDC_TFT_MODE; /* no monochrome/passive support */ +	if (info->tft_alt_mode) +		reg |= LCDC_TFT_ALT_ENABLE; +	if (priv->rev == 2) { +		unsigned int depth, bpp; + +		drm_fb_get_bpp_depth(crtc->fb->pixel_format, &depth, &bpp); +		switch (bpp) { +		case 16: +			break; +		case 32: +			reg |= LCDC_V2_TFT_24BPP_UNPACK; +			/* fallthrough */ +		case 24: +			reg |= LCDC_V2_TFT_24BPP_MODE; +			break; +		default: +			dev_err(dev->dev, "invalid pixel format\n"); +			return -EINVAL; +		} +	} +	reg |= info->fdd < 12; +	tilcdc_write(dev, LCDC_RASTER_CTRL_REG, reg); + +	if (info->invert_pxl_clk) +		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); + +	if (info->sync_ctrl) +		tilcdc_set(dev, LCDC_RASTER_TIMING_2_REG, LCDC_SYNC_CTRL); +	else +		tilcdc_clear(dev, LCDC_RASTER_TIMING_2_REG, LCDC_SYNC_CTRL); + +	if (info->sync_edge) +		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); + +	if (info->raster_order) +		tilcdc_set(dev, LCDC_RASTER_CTRL_REG, LCDC_RASTER_ORDER); +	else +		tilcdc_clear(dev, LCDC_RASTER_CTRL_REG, LCDC_RASTER_ORDER); + + +	update_scanout(crtc); +	tilcdc_crtc_update_clk(crtc); + +	pm_runtime_put_sync(dev->dev); + +	return 0; +} + +static int tilcdc_crtc_mode_set_base(struct drm_crtc *crtc, int x, int y, +		struct drm_framebuffer *old_fb) +{ +	update_scanout(crtc); +	return 0; +} + +static void tilcdc_crtc_load_lut(struct drm_crtc *crtc) +{ +} + +static const struct drm_crtc_funcs tilcdc_crtc_funcs = { +		.destroy        = tilcdc_crtc_destroy, +		.set_config     = drm_crtc_helper_set_config, +		.page_flip      = tilcdc_crtc_page_flip, +}; + +static const struct drm_crtc_helper_funcs tilcdc_crtc_helper_funcs = { +		.dpms           = tilcdc_crtc_dpms, +		.mode_fixup     = tilcdc_crtc_mode_fixup, +		.prepare        = tilcdc_crtc_prepare, +		.commit         = tilcdc_crtc_commit, +		.mode_set       = tilcdc_crtc_mode_set, +		.mode_set_base  = tilcdc_crtc_mode_set_base, +		.load_lut       = tilcdc_crtc_load_lut, +}; + +int tilcdc_crtc_max_width(struct drm_crtc *crtc) +{ +	struct drm_device *dev = crtc->dev; +	struct tilcdc_drm_private *priv = dev->dev_private; +	int max_width = 0; + +	if (priv->rev == 1) +		max_width = 1024; +	else if (priv->rev == 2) +		max_width = 2048; + +	return max_width; +} + +int tilcdc_crtc_mode_valid(struct drm_crtc *crtc, struct drm_display_mode *mode) +{ +	struct tilcdc_drm_private *priv = crtc->dev->dev_private; +	unsigned int bandwidth; + +	if (mode->hdisplay > tilcdc_crtc_max_width(crtc)) +		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; + +	/* filter out modes that would require too much memory bandwidth: */ +	bandwidth = mode->hdisplay * mode->vdisplay * drm_mode_vrefresh(mode); +	if (bandwidth > priv->max_bandwidth) +		return MODE_BAD; + +	return MODE_OK; +} + +void tilcdc_crtc_set_panel_info(struct drm_crtc *crtc, +		const struct tilcdc_panel_info *info) +{ +	struct tilcdc_crtc *tilcdc_crtc = to_tilcdc_crtc(crtc); +	tilcdc_crtc->info = info; +} + +void tilcdc_crtc_update_clk(struct drm_crtc *crtc) +{ +	struct tilcdc_crtc *tilcdc_crtc = to_tilcdc_crtc(crtc); +	struct drm_device *dev = crtc->dev; +	struct tilcdc_drm_private *priv = dev->dev_private; +	int dpms = tilcdc_crtc->dpms; +	unsigned int lcd_clk, div; +	int ret; + +	pm_runtime_get_sync(dev->dev); + +	if (dpms == DRM_MODE_DPMS_ON) +		tilcdc_crtc_dpms(crtc, DRM_MODE_DPMS_OFF); + +	/* in raster mode, minimum divisor is 2: */ +	ret = clk_set_rate(priv->disp_clk, crtc->mode.clock * 1000 * 2); +	if (ret) { +		dev_err(dev->dev, "failed to set display clock rate to: %d\n", +				crtc->mode.clock); +		goto out; +	} + +	lcd_clk = clk_get_rate(priv->clk); +	div = lcd_clk / (crtc->mode.clock * 1000); + +	DBG("lcd_clk=%u, mode clock=%d, div=%u", lcd_clk, crtc->mode.clock, div); +	DBG("fck=%lu, dpll_disp_ck=%lu", clk_get_rate(priv->clk), clk_get_rate(priv->disp_clk)); + +	/* Configure the LCD clock divisor. */ +	tilcdc_write(dev, LCDC_CTRL_REG, LCDC_CLK_DIVISOR(div) | +			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); + +	if (dpms == DRM_MODE_DPMS_ON) +		tilcdc_crtc_dpms(crtc, DRM_MODE_DPMS_ON); + +out: +	pm_runtime_put_sync(dev->dev); +} + +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 = dev->dev_private; +	uint32_t stat = tilcdc_read_irqstatus(dev); + +	if ((stat & LCDC_SYNC_LOST) && (stat & LCDC_FIFO_UNDERFLOW)) { +		stop(crtc); +		dev_err(dev->dev, "error: %08x\n", stat); +		tilcdc_clear_irqstatus(dev, stat); +		start(crtc); +	} else if (stat & LCDC_PL_LOAD_DONE) { +		tilcdc_clear_irqstatus(dev, stat); +	} else { +		struct drm_pending_vblank_event *event; +		unsigned long flags; +		uint32_t dirty = tilcdc_crtc->dirty & stat; + +		tilcdc_clear_irqstatus(dev, stat); + +		if (dirty & LCDC_END_OF_FRAME0) +			set_scanout(crtc, 0); + +		if (dirty & LCDC_END_OF_FRAME1) +			set_scanout(crtc, 1); + +		drm_handle_vblank(dev, 0); + +		spin_lock_irqsave(&dev->event_lock, flags); +		event = tilcdc_crtc->event; +		tilcdc_crtc->event = NULL; +		if (event) +			drm_send_vblank_event(dev, 0, event); +		spin_unlock_irqrestore(&dev->event_lock, flags); + +		if (dirty && !tilcdc_crtc->dirty) +			drm_vblank_put(dev, 0); +	} + +	if (priv->rev == 2) { +		if (stat & LCDC_FRAME_DONE) { +			tilcdc_crtc->frame_done = true; +			wake_up(&tilcdc_crtc->frame_done_wq); +		} +		tilcdc_write(dev, LCDC_END_OF_INT_IND_REG, 0); +	} + +	return IRQ_HANDLED; +} + +void tilcdc_crtc_cancel_page_flip(struct drm_crtc *crtc, struct drm_file *file) +{ +	struct tilcdc_crtc *tilcdc_crtc = to_tilcdc_crtc(crtc); +	struct drm_pending_vblank_event *event; +	struct drm_device *dev = crtc->dev; +	unsigned long flags; + +	/* Destroy the pending vertical blanking event associated with the +	 * pending page flip, if any, and disable vertical blanking interrupts. +	 */ +	spin_lock_irqsave(&dev->event_lock, flags); +	event = tilcdc_crtc->event; +	if (event && event->base.file_priv == file) { +		tilcdc_crtc->event = NULL; +		event->base.destroy(&event->base); +		drm_vblank_put(dev, 0); +	} +	spin_unlock_irqrestore(&dev->event_lock, flags); +} + +struct drm_crtc *tilcdc_crtc_create(struct drm_device *dev) +{ +	struct tilcdc_crtc *tilcdc_crtc; +	struct drm_crtc *crtc; +	int ret; + +	tilcdc_crtc = kzalloc(sizeof(*tilcdc_crtc), GFP_KERNEL); +	if (!tilcdc_crtc) { +		dev_err(dev->dev, "allocation failed\n"); +		return NULL; +	} + +	crtc = &tilcdc_crtc->base; + +	tilcdc_crtc->dpms = DRM_MODE_DPMS_OFF; +	init_waitqueue_head(&tilcdc_crtc->frame_done_wq); + +	ret = kfifo_alloc(&tilcdc_crtc->unref_fifo, 16, GFP_KERNEL); +	if (ret) { +		dev_err(dev->dev, "could not allocate unref FIFO\n"); +		goto fail; +	} + +	INIT_WORK(&tilcdc_crtc->work, unref_worker); + +	ret = drm_crtc_init(dev, crtc, &tilcdc_crtc_funcs); +	if (ret < 0) +		goto fail; + +	drm_crtc_helper_add(crtc, &tilcdc_crtc_helper_funcs); + +	return crtc; + +fail: +	tilcdc_crtc_destroy(crtc); +	return NULL; +} |