diff options
Diffstat (limited to 'drivers/media/platform/nxp/imx8-isi/imx8-isi-hw.c')
-rw-r--r-- | drivers/media/platform/nxp/imx8-isi/imx8-isi-hw.c | 650 |
1 files changed, 650 insertions, 0 deletions
diff --git a/drivers/media/platform/nxp/imx8-isi/imx8-isi-hw.c b/drivers/media/platform/nxp/imx8-isi/imx8-isi-hw.c new file mode 100644 index 000000000000..db538f3d88ec --- /dev/null +++ b/drivers/media/platform/nxp/imx8-isi/imx8-isi-hw.c @@ -0,0 +1,650 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright 2019-2020 NXP + */ + +#include <linux/delay.h> +#include <linux/device.h> +#include <linux/io.h> +#include <linux/types.h> + +#include "imx8-isi-core.h" +#include "imx8-isi-regs.h" + +#define ISI_DOWNSCALE_THRESHOLD 0x4000 + +static inline u32 mxc_isi_read(struct mxc_isi_pipe *pipe, u32 reg) +{ + return readl(pipe->regs + reg); +} + +static inline void mxc_isi_write(struct mxc_isi_pipe *pipe, u32 reg, u32 val) +{ + writel(val, pipe->regs + reg); +} + +/* ----------------------------------------------------------------------------- + * Buffers & M2M operation + */ + +void mxc_isi_channel_set_inbuf(struct mxc_isi_pipe *pipe, dma_addr_t dma_addr) +{ + mxc_isi_write(pipe, CHNL_IN_BUF_ADDR, dma_addr); +#if CONFIG_ARCH_DMA_ADDR_T_64BIT + if (pipe->isi->pdata->has_36bit_dma) + mxc_isi_write(pipe, CHNL_IN_BUF_XTND_ADDR, dma_addr >> 32); +#endif +} + +void mxc_isi_channel_set_outbuf(struct mxc_isi_pipe *pipe, + const dma_addr_t dma_addrs[3], + enum mxc_isi_buf_id buf_id) +{ + int val; + + val = mxc_isi_read(pipe, CHNL_OUT_BUF_CTRL); + + if (buf_id == MXC_ISI_BUF1) { + mxc_isi_write(pipe, CHNL_OUT_BUF1_ADDR_Y, dma_addrs[0]); + mxc_isi_write(pipe, CHNL_OUT_BUF1_ADDR_U, dma_addrs[1]); + mxc_isi_write(pipe, CHNL_OUT_BUF1_ADDR_V, dma_addrs[2]); +#if CONFIG_ARCH_DMA_ADDR_T_64BIT + if (pipe->isi->pdata->has_36bit_dma) { + mxc_isi_write(pipe, CHNL_Y_BUF1_XTND_ADDR, + dma_addrs[0] >> 32); + mxc_isi_write(pipe, CHNL_U_BUF1_XTND_ADDR, + dma_addrs[1] >> 32); + mxc_isi_write(pipe, CHNL_V_BUF1_XTND_ADDR, + dma_addrs[2] >> 32); + } +#endif + val ^= CHNL_OUT_BUF_CTRL_LOAD_BUF1_ADDR; + } else { + mxc_isi_write(pipe, CHNL_OUT_BUF2_ADDR_Y, dma_addrs[0]); + mxc_isi_write(pipe, CHNL_OUT_BUF2_ADDR_U, dma_addrs[1]); + mxc_isi_write(pipe, CHNL_OUT_BUF2_ADDR_V, dma_addrs[2]); +#if CONFIG_ARCH_DMA_ADDR_T_64BIT + if (pipe->isi->pdata->has_36bit_dma) { + mxc_isi_write(pipe, CHNL_Y_BUF2_XTND_ADDR, + dma_addrs[0] >> 32); + mxc_isi_write(pipe, CHNL_U_BUF2_XTND_ADDR, + dma_addrs[1] >> 32); + mxc_isi_write(pipe, CHNL_V_BUF2_XTND_ADDR, + dma_addrs[2] >> 32); + } +#endif + val ^= CHNL_OUT_BUF_CTRL_LOAD_BUF2_ADDR; + } + + mxc_isi_write(pipe, CHNL_OUT_BUF_CTRL, val); +} + +void mxc_isi_channel_m2m_start(struct mxc_isi_pipe *pipe) +{ + u32 val; + + val = mxc_isi_read(pipe, CHNL_MEM_RD_CTRL); + val &= ~CHNL_MEM_RD_CTRL_READ_MEM; + mxc_isi_write(pipe, CHNL_MEM_RD_CTRL, val); + + fsleep(300); + + val |= CHNL_MEM_RD_CTRL_READ_MEM; + mxc_isi_write(pipe, CHNL_MEM_RD_CTRL, val); +} + +/* ----------------------------------------------------------------------------- + * Pipeline configuration + */ + +static u32 mxc_isi_channel_scaling_ratio(unsigned int from, unsigned int to, + u32 *dec) +{ + unsigned int ratio = from / to; + + if (ratio < 2) + *dec = 1; + else if (ratio < 4) + *dec = 2; + else if (ratio < 8) + *dec = 4; + else + *dec = 8; + + return min_t(u32, from * 0x1000 / (to * *dec), ISI_DOWNSCALE_THRESHOLD); +} + +static void mxc_isi_channel_set_scaling(struct mxc_isi_pipe *pipe, + enum mxc_isi_encoding encoding, + const struct v4l2_area *in_size, + const struct v4l2_area *out_size, + bool *bypass) +{ + u32 xscale, yscale; + u32 decx, decy; + u32 val; + + dev_dbg(pipe->isi->dev, "input %ux%u, output %ux%u\n", + in_size->width, in_size->height, + out_size->width, out_size->height); + + xscale = mxc_isi_channel_scaling_ratio(in_size->width, out_size->width, + &decx); + yscale = mxc_isi_channel_scaling_ratio(in_size->height, out_size->height, + &decy); + + val = mxc_isi_read(pipe, CHNL_IMG_CTRL); + val &= ~(CHNL_IMG_CTRL_DEC_X_MASK | CHNL_IMG_CTRL_DEC_Y_MASK | + CHNL_IMG_CTRL_YCBCR_MODE); + + val |= CHNL_IMG_CTRL_DEC_X(ilog2(decx)) + | CHNL_IMG_CTRL_DEC_Y(ilog2(decy)); + + /* + * Contrary to what the documentation states, YCBCR_MODE does not + * control conversion between YCbCr and RGB, but whether the scaler + * operates in YUV mode or in RGB mode. It must be set when the scaler + * input is YUV. + */ + if (encoding == MXC_ISI_ENC_YUV) + val |= CHNL_IMG_CTRL_YCBCR_MODE; + + mxc_isi_write(pipe, CHNL_IMG_CTRL, val); + + mxc_isi_write(pipe, CHNL_SCALE_FACTOR, + CHNL_SCALE_FACTOR_Y_SCALE(yscale) | + CHNL_SCALE_FACTOR_X_SCALE(xscale)); + + mxc_isi_write(pipe, CHNL_SCALE_OFFSET, 0); + + mxc_isi_write(pipe, CHNL_SCL_IMG_CFG, + CHNL_SCL_IMG_CFG_HEIGHT(out_size->height) | + CHNL_SCL_IMG_CFG_WIDTH(out_size->width)); + + *bypass = in_size->height == out_size->height && + in_size->width == out_size->width; +} + +static void mxc_isi_channel_set_crop(struct mxc_isi_pipe *pipe, + const struct v4l2_area *src, + const struct v4l2_rect *dst) +{ + u32 val, val0, val1; + + val = mxc_isi_read(pipe, CHNL_IMG_CTRL); + val &= ~CHNL_IMG_CTRL_CROP_EN; + + if (src->height == dst->height && src->width == dst->width) { + mxc_isi_write(pipe, CHNL_IMG_CTRL, val); + return; + } + + val |= CHNL_IMG_CTRL_CROP_EN; + val0 = CHNL_CROP_ULC_X(dst->left) | CHNL_CROP_ULC_Y(dst->top); + val1 = CHNL_CROP_LRC_X(dst->width) | CHNL_CROP_LRC_Y(dst->height); + + mxc_isi_write(pipe, CHNL_CROP_ULC, val0); + mxc_isi_write(pipe, CHNL_CROP_LRC, val1 + val0); + mxc_isi_write(pipe, CHNL_IMG_CTRL, val); +} + +/* + * A2,A1, B1, A3, B3, B2, + * C2, C1, D1, C3, D3, D2 + */ +static const u32 mxc_isi_yuv2rgb_coeffs[6] = { + /* YUV -> RGB */ + 0x0000012a, 0x012a0198, 0x0730079c, + 0x0204012a, 0x01f00000, 0x01800180 +}; + +static const u32 mxc_isi_rgb2yuv_coeffs[6] = { + /* RGB->YUV */ + 0x00810041, 0x07db0019, 0x007007b6, + 0x07a20070, 0x001007ee, 0x00800080 +}; + +static void mxc_isi_channel_set_csc(struct mxc_isi_pipe *pipe, + enum mxc_isi_encoding in_encoding, + enum mxc_isi_encoding out_encoding, + bool *bypass) +{ + static const char * const encodings[] = { + [MXC_ISI_ENC_RAW] = "RAW", + [MXC_ISI_ENC_RGB] = "RGB", + [MXC_ISI_ENC_YUV] = "YUV", + }; + const u32 *coeffs; + bool cscen = true; + u32 val; + + val = mxc_isi_read(pipe, CHNL_IMG_CTRL); + val &= ~(CHNL_IMG_CTRL_CSC_BYPASS | CHNL_IMG_CTRL_CSC_MODE_MASK); + + if (in_encoding == MXC_ISI_ENC_YUV && + out_encoding == MXC_ISI_ENC_RGB) { + /* YUV2RGB */ + coeffs = mxc_isi_yuv2rgb_coeffs; + /* YCbCr enable??? */ + val |= CHNL_IMG_CTRL_CSC_MODE(CHNL_IMG_CTRL_CSC_MODE_YCBCR2RGB); + } else if (in_encoding == MXC_ISI_ENC_RGB && + out_encoding == MXC_ISI_ENC_YUV) { + /* RGB2YUV */ + coeffs = mxc_isi_rgb2yuv_coeffs; + val |= CHNL_IMG_CTRL_CSC_MODE(CHNL_IMG_CTRL_CSC_MODE_RGB2YCBCR); + } else { + /* Bypass CSC */ + cscen = false; + val |= CHNL_IMG_CTRL_CSC_BYPASS; + } + + dev_dbg(pipe->isi->dev, "CSC: %s -> %s\n", + encodings[in_encoding], encodings[out_encoding]); + + if (cscen) { + mxc_isi_write(pipe, CHNL_CSC_COEFF0, coeffs[0]); + mxc_isi_write(pipe, CHNL_CSC_COEFF1, coeffs[1]); + mxc_isi_write(pipe, CHNL_CSC_COEFF2, coeffs[2]); + mxc_isi_write(pipe, CHNL_CSC_COEFF3, coeffs[3]); + mxc_isi_write(pipe, CHNL_CSC_COEFF4, coeffs[4]); + mxc_isi_write(pipe, CHNL_CSC_COEFF5, coeffs[5]); + } + + mxc_isi_write(pipe, CHNL_IMG_CTRL, val); + + *bypass = !cscen; +} + +void mxc_isi_channel_set_alpha(struct mxc_isi_pipe *pipe, u8 alpha) +{ + u32 val; + + val = mxc_isi_read(pipe, CHNL_IMG_CTRL); + val &= ~CHNL_IMG_CTRL_GBL_ALPHA_VAL_MASK; + val |= CHNL_IMG_CTRL_GBL_ALPHA_VAL(alpha) | + CHNL_IMG_CTRL_GBL_ALPHA_EN; + mxc_isi_write(pipe, CHNL_IMG_CTRL, val); +} + +void mxc_isi_channel_set_flip(struct mxc_isi_pipe *pipe, bool hflip, bool vflip) +{ + u32 val; + + val = mxc_isi_read(pipe, CHNL_IMG_CTRL); + val &= ~(CHNL_IMG_CTRL_VFLIP_EN | CHNL_IMG_CTRL_HFLIP_EN); + + if (vflip) + val |= CHNL_IMG_CTRL_VFLIP_EN; + if (hflip) + val |= CHNL_IMG_CTRL_HFLIP_EN; + + mxc_isi_write(pipe, CHNL_IMG_CTRL, val); +} + +static void mxc_isi_channel_set_panic_threshold(struct mxc_isi_pipe *pipe) +{ + const struct mxc_isi_set_thd *set_thd = pipe->isi->pdata->set_thd; + u32 val; + + val = mxc_isi_read(pipe, CHNL_OUT_BUF_CTRL); + + val &= ~(set_thd->panic_set_thd_y.mask); + val |= set_thd->panic_set_thd_y.threshold << set_thd->panic_set_thd_y.offset; + + val &= ~(set_thd->panic_set_thd_u.mask); + val |= set_thd->panic_set_thd_u.threshold << set_thd->panic_set_thd_u.offset; + + val &= ~(set_thd->panic_set_thd_v.mask); + val |= set_thd->panic_set_thd_v.threshold << set_thd->panic_set_thd_v.offset; + + mxc_isi_write(pipe, CHNL_OUT_BUF_CTRL, val); +} + +static void mxc_isi_channel_set_control(struct mxc_isi_pipe *pipe, + enum mxc_isi_input_id input, + bool bypass) +{ + u32 val; + + mutex_lock(&pipe->lock); + + val = mxc_isi_read(pipe, CHNL_CTRL); + val &= ~(CHNL_CTRL_CHNL_BYPASS | CHNL_CTRL_CHAIN_BUF_MASK | + CHNL_CTRL_BLANK_PXL_MASK | CHNL_CTRL_SRC_TYPE_MASK | + CHNL_CTRL_MIPI_VC_ID_MASK | CHNL_CTRL_SRC_INPUT_MASK); + + /* + * If no scaling or color space conversion is needed, bypass the + * channel. + */ + if (bypass) + val |= CHNL_CTRL_CHNL_BYPASS; + + /* Chain line buffers if needed. */ + if (pipe->chained) + val |= CHNL_CTRL_CHAIN_BUF(CHNL_CTRL_CHAIN_BUF_2_CHAIN); + + val |= CHNL_CTRL_BLANK_PXL(0xff); + + /* Input source (including VC configuration for CSI-2) */ + if (input == MXC_ISI_INPUT_MEM) { + /* + * The memory input is connected to the last port of the + * crossbar switch, after all pixel link inputs. The SRC_INPUT + * field controls the input selection and must be set + * accordingly, despite being documented as ignored when using + * the memory input in the i.MX8MP reference manual, and + * reserved in the i.MX8MN reference manual. + */ + val |= CHNL_CTRL_SRC_TYPE(CHNL_CTRL_SRC_TYPE_MEMORY); + val |= CHNL_CTRL_SRC_INPUT(pipe->isi->pdata->num_ports); + } else { + val |= CHNL_CTRL_SRC_TYPE(CHNL_CTRL_SRC_TYPE_DEVICE); + val |= CHNL_CTRL_SRC_INPUT(input); + val |= CHNL_CTRL_MIPI_VC_ID(0); /* FIXME: For CSI-2 only */ + } + + mxc_isi_write(pipe, CHNL_CTRL, val); + + mutex_unlock(&pipe->lock); +} + +void mxc_isi_channel_config(struct mxc_isi_pipe *pipe, + enum mxc_isi_input_id input, + const struct v4l2_area *in_size, + const struct v4l2_area *scale, + const struct v4l2_rect *crop, + enum mxc_isi_encoding in_encoding, + enum mxc_isi_encoding out_encoding) +{ + bool csc_bypass; + bool scaler_bypass; + + /* Input frame size */ + mxc_isi_write(pipe, CHNL_IMG_CFG, + CHNL_IMG_CFG_HEIGHT(in_size->height) | + CHNL_IMG_CFG_WIDTH(in_size->width)); + + /* Scaling */ + mxc_isi_channel_set_scaling(pipe, in_encoding, in_size, scale, + &scaler_bypass); + mxc_isi_channel_set_crop(pipe, scale, crop); + + /* CSC */ + mxc_isi_channel_set_csc(pipe, in_encoding, out_encoding, &csc_bypass); + + /* Output buffer management */ + mxc_isi_channel_set_panic_threshold(pipe); + + /* Channel control */ + mxc_isi_channel_set_control(pipe, input, csc_bypass && scaler_bypass); +} + +void mxc_isi_channel_set_input_format(struct mxc_isi_pipe *pipe, + const struct mxc_isi_format_info *info, + const struct v4l2_pix_format_mplane *format) +{ + unsigned int bpl = format->plane_fmt[0].bytesperline; + + mxc_isi_write(pipe, CHNL_MEM_RD_CTRL, + CHNL_MEM_RD_CTRL_IMG_TYPE(info->isi_in_format)); + mxc_isi_write(pipe, CHNL_IN_BUF_PITCH, + CHNL_IN_BUF_PITCH_LINE_PITCH(bpl)); +} + +void mxc_isi_channel_set_output_format(struct mxc_isi_pipe *pipe, + const struct mxc_isi_format_info *info, + struct v4l2_pix_format_mplane *format) +{ + u32 val; + + /* set outbuf format */ + dev_dbg(pipe->isi->dev, "output format %p4cc", &format->pixelformat); + + val = mxc_isi_read(pipe, CHNL_IMG_CTRL); + val &= ~CHNL_IMG_CTRL_FORMAT_MASK; + val |= CHNL_IMG_CTRL_FORMAT(info->isi_out_format); + mxc_isi_write(pipe, CHNL_IMG_CTRL, val); + + /* line pitch */ + mxc_isi_write(pipe, CHNL_OUT_BUF_PITCH, + format->plane_fmt[0].bytesperline); +} + +/* ----------------------------------------------------------------------------- + * IRQ + */ + +u32 mxc_isi_channel_irq_status(struct mxc_isi_pipe *pipe, bool clear) +{ + u32 status; + + status = mxc_isi_read(pipe, CHNL_STS); + if (clear) + mxc_isi_write(pipe, CHNL_STS, status); + + return status; +} + +void mxc_isi_channel_irq_clear(struct mxc_isi_pipe *pipe) +{ + mxc_isi_write(pipe, CHNL_STS, 0xffffffff); +} + +static void mxc_isi_channel_irq_enable(struct mxc_isi_pipe *pipe) +{ + const struct mxc_isi_ier_reg *ier_reg = pipe->isi->pdata->ier_reg; + u32 val; + + val = CHNL_IER_FRM_RCVD_EN | + CHNL_IER_AXI_WR_ERR_U_EN | + CHNL_IER_AXI_WR_ERR_V_EN | + CHNL_IER_AXI_WR_ERR_Y_EN; + + /* Y/U/V overflow enable */ + val |= ier_reg->oflw_y_buf_en.mask | + ier_reg->oflw_u_buf_en.mask | + ier_reg->oflw_v_buf_en.mask; + + /* Y/U/V excess overflow enable */ + val |= ier_reg->excs_oflw_y_buf_en.mask | + ier_reg->excs_oflw_u_buf_en.mask | + ier_reg->excs_oflw_v_buf_en.mask; + + /* Y/U/V panic enable */ + val |= ier_reg->panic_y_buf_en.mask | + ier_reg->panic_u_buf_en.mask | + ier_reg->panic_v_buf_en.mask; + + mxc_isi_channel_irq_clear(pipe); + mxc_isi_write(pipe, CHNL_IER, val); +} + +static void mxc_isi_channel_irq_disable(struct mxc_isi_pipe *pipe) +{ + mxc_isi_write(pipe, CHNL_IER, 0); +} + +/* ----------------------------------------------------------------------------- + * Init, deinit, enable, disable + */ + +static void mxc_isi_channel_sw_reset(struct mxc_isi_pipe *pipe, bool enable_clk) +{ + mxc_isi_write(pipe, CHNL_CTRL, CHNL_CTRL_SW_RST); + mdelay(5); + mxc_isi_write(pipe, CHNL_CTRL, enable_clk ? CHNL_CTRL_CLK_EN : 0); +} + +static void __mxc_isi_channel_get(struct mxc_isi_pipe *pipe) +{ + if (!pipe->use_count++) + mxc_isi_channel_sw_reset(pipe, true); +} + +void mxc_isi_channel_get(struct mxc_isi_pipe *pipe) +{ + mutex_lock(&pipe->lock); + __mxc_isi_channel_get(pipe); + mutex_unlock(&pipe->lock); +} + +static void __mxc_isi_channel_put(struct mxc_isi_pipe *pipe) +{ + if (!--pipe->use_count) + mxc_isi_channel_sw_reset(pipe, false); +} + +void mxc_isi_channel_put(struct mxc_isi_pipe *pipe) +{ + mutex_lock(&pipe->lock); + __mxc_isi_channel_put(pipe); + mutex_unlock(&pipe->lock); +} + +void mxc_isi_channel_enable(struct mxc_isi_pipe *pipe) +{ + u32 val; + + mxc_isi_channel_irq_enable(pipe); + + mutex_lock(&pipe->lock); + + val = mxc_isi_read(pipe, CHNL_CTRL); + val |= CHNL_CTRL_CHNL_EN; + mxc_isi_write(pipe, CHNL_CTRL, val); + + mutex_unlock(&pipe->lock); +} + +void mxc_isi_channel_disable(struct mxc_isi_pipe *pipe) +{ + u32 val; + + mxc_isi_channel_irq_disable(pipe); + + mutex_lock(&pipe->lock); + + val = mxc_isi_read(pipe, CHNL_CTRL); + val &= ~CHNL_CTRL_CHNL_EN; + mxc_isi_write(pipe, CHNL_CTRL, val); + + mutex_unlock(&pipe->lock); +} + +/* ----------------------------------------------------------------------------- + * Resource management & chaining + */ +int mxc_isi_channel_acquire(struct mxc_isi_pipe *pipe, + mxc_isi_pipe_irq_t irq_handler, bool bypass) +{ + u8 resources; + int ret = 0; + + mutex_lock(&pipe->lock); + + if (pipe->irq_handler) { + ret = -EBUSY; + goto unlock; + } + + /* + * Make sure the resources we need are available. The output buffer is + * always needed to operate the channel, the line buffer is needed only + * when the channel isn't in bypass mode. + */ + resources = MXC_ISI_CHANNEL_RES_OUTPUT_BUF + | (!bypass ? MXC_ISI_CHANNEL_RES_LINE_BUF : 0); + if ((pipe->available_res & resources) != resources) { + ret = -EBUSY; + goto unlock; + } + + /* Acquire the channel resources. */ + pipe->acquired_res = resources; + pipe->available_res &= ~resources; + pipe->irq_handler = irq_handler; + +unlock: + mutex_unlock(&pipe->lock); + + return ret; +} + +void mxc_isi_channel_release(struct mxc_isi_pipe *pipe) +{ + mutex_lock(&pipe->lock); + + pipe->irq_handler = NULL; + pipe->available_res |= pipe->acquired_res; + pipe->acquired_res = 0; + + mutex_unlock(&pipe->lock); +} + +/* + * We currently support line buffer chaining only, for handling images with a + * width larger than 2048 pixels. + * + * TODO: Support secondary line buffer for downscaling YUV420 images. + */ +int mxc_isi_channel_chain(struct mxc_isi_pipe *pipe, bool bypass) +{ + /* Channel chaining requires both line and output buffer. */ + const u8 resources = MXC_ISI_CHANNEL_RES_OUTPUT_BUF + | MXC_ISI_CHANNEL_RES_LINE_BUF; + struct mxc_isi_pipe *chained_pipe = pipe + 1; + int ret = 0; + + /* + * If buffer chaining is required, make sure this channel is not the + * last one, otherwise there's no 'next' channel to chain with. This + * should be prevented by checks in the set format handlers, but let's + * be defensive. + */ + if (WARN_ON(pipe->id == pipe->isi->pdata->num_channels - 1)) + return -EINVAL; + + mutex_lock(&chained_pipe->lock); + + /* Safety checks. */ + if (WARN_ON(pipe->chained || chained_pipe->chained_res)) { + ret = -EINVAL; + goto unlock; + } + + if ((chained_pipe->available_res & resources) != resources) { + ret = -EBUSY; + goto unlock; + } + + pipe->chained = true; + chained_pipe->chained_res |= resources; + chained_pipe->available_res &= ~resources; + + __mxc_isi_channel_get(chained_pipe); + +unlock: + mutex_unlock(&chained_pipe->lock); + + return ret; +} + +void mxc_isi_channel_unchain(struct mxc_isi_pipe *pipe) +{ + struct mxc_isi_pipe *chained_pipe = pipe + 1; + + if (!pipe->chained) + return; + + pipe->chained = false; + + mutex_lock(&chained_pipe->lock); + + chained_pipe->available_res |= chained_pipe->chained_res; + chained_pipe->chained_res = 0; + + __mxc_isi_channel_put(chained_pipe); + + mutex_unlock(&chained_pipe->lock); +} |