/* * Copyright 2004-2010 Freescale Semiconductor, Inc. All Rights Reserved. */ /* * The code contained herein is licensed under the GNU General Public * License. You may obtain a copy of the GNU General Public License * Version 2 or later at the following locations: * * http://www.opensource.org/licenses/gpl-license.html * http://www.gnu.org/copyleft/gpl.html */ /*! * @defgroup Framebuffer Framebuffer Driver for SDC and ADC. */ /*! * @file mxcfb.c * * @brief MXC Frame buffer driver for SDC * * @ingroup Framebuffer */ /*! * Include files */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* * Driver name */ #define MXCFB_NAME "mxc_sdc_fb" /*! * Structure containing the MXC specific framebuffer information. */ struct mxcfb_info { int blank; ipu_channel_t ipu_ch; int ipu_di; u32 ipu_di_pix_fmt; bool overlay; bool alpha_chan_en; dma_addr_t alpha_phy_addr0; dma_addr_t alpha_phy_addr1; void *alpha_virt_addr0; void *alpha_virt_addr1; uint32_t alpha_mem_len; uint32_t ipu_ch_irq; uint32_t cur_ipu_buf; uint32_t cur_ipu_alpha_buf; u32 pseudo_palette[16]; struct semaphore flip_sem; struct semaphore alpha_flip_sem; struct completion vsync_complete; }; struct mxcfb_alloc_list { struct list_head list; dma_addr_t phy_addr; void *cpu_addr; u32 size; }; enum { BOTH_ON, SRC_ON, TGT_ON, BOTH_OFF }; static char *fb_mode; static unsigned long default_bpp = 16; static bool g_dp_in_use; LIST_HEAD(fb_alloc_list); static struct fb_info *mxcfb_info[3]; static int ext_clk_used; static uint32_t bpp_to_pixfmt(struct fb_info *fbi) { uint32_t pixfmt = 0; if (fbi->var.nonstd) return fbi->var.nonstd; switch (fbi->var.bits_per_pixel) { case 24: pixfmt = IPU_PIX_FMT_BGR24; break; case 32: pixfmt = IPU_PIX_FMT_BGR32; break; case 16: pixfmt = IPU_PIX_FMT_RGB565; break; } return pixfmt; } static irqreturn_t mxcfb_irq_handler(int irq, void *dev_id); static int mxcfb_blank(int blank, struct fb_info *info); static int mxcfb_map_video_memory(struct fb_info *fbi); static int mxcfb_unmap_video_memory(struct fb_info *fbi); /* * Set fixed framebuffer parameters based on variable settings. * * @param info framebuffer information pointer */ static int mxcfb_set_fix(struct fb_info *info) { struct fb_fix_screeninfo *fix = &info->fix; struct fb_var_screeninfo *var = &info->var; fix->line_length = var->xres_virtual * var->bits_per_pixel / 8; fix->type = FB_TYPE_PACKED_PIXELS; fix->accel = FB_ACCEL_NONE; fix->visual = FB_VISUAL_TRUECOLOR; fix->xpanstep = 1; fix->ypanstep = 1; return 0; } static int _setup_disp_channel1(struct fb_info *fbi) { ipu_channel_params_t params; struct mxcfb_info *mxc_fbi = (struct mxcfb_info *)fbi->par; memset(¶ms, 0, sizeof(params)); params.mem_dp_bg_sync.di = mxc_fbi->ipu_di; /* * Assuming interlaced means yuv output, below setting also * valid for mem_dc_sync. FG should have the same vmode as BG. */ if (mxc_fbi->ipu_ch == MEM_FG_SYNC) { struct mxcfb_info *mxc_fbi_tmp; int i; for (i = 0; i < num_registered_fb; i++) { mxc_fbi_tmp = (struct mxcfb_info *) (registered_fb[i]->par); if (mxc_fbi_tmp->ipu_ch == MEM_BG_SYNC) { fbi->var.vmode = registered_fb[i]->var.vmode; mxc_fbi->ipu_di_pix_fmt = mxc_fbi_tmp->ipu_di_pix_fmt; break; } } } if (fbi->var.vmode & FB_VMODE_INTERLACED) { params.mem_dp_bg_sync.interlaced = true; params.mem_dp_bg_sync.out_pixel_fmt = IPU_PIX_FMT_YUV444; } else { if (mxc_fbi->ipu_di_pix_fmt) params.mem_dp_bg_sync.out_pixel_fmt = mxc_fbi->ipu_di_pix_fmt; else params.mem_dp_bg_sync.out_pixel_fmt = IPU_PIX_FMT_RGB666; } params.mem_dp_bg_sync.in_pixel_fmt = bpp_to_pixfmt(fbi); if (mxc_fbi->alpha_chan_en) params.mem_dp_bg_sync.alpha_chan_en = true; ipu_init_channel(mxc_fbi->ipu_ch, ¶ms); return 0; } static int _setup_disp_channel2(struct fb_info *fbi) { int retval = 0; struct mxcfb_info *mxc_fbi = (struct mxcfb_info *)fbi->par; mxc_fbi->cur_ipu_buf = 1; sema_init(&mxc_fbi->flip_sem, 1); if (mxc_fbi->alpha_chan_en) { mxc_fbi->cur_ipu_alpha_buf = 1; sema_init(&mxc_fbi->alpha_flip_sem, 1); } fbi->var.xoffset = fbi->var.yoffset = 0; retval = ipu_init_channel_buffer(mxc_fbi->ipu_ch, IPU_INPUT_BUFFER, bpp_to_pixfmt(fbi), fbi->var.xres, fbi->var.yres, fbi->fix.line_length, IPU_ROTATE_NONE, fbi->fix.smem_start + (fbi->fix.line_length * fbi->var.yres), fbi->fix.smem_start, 0, 0); if (retval) { dev_err(fbi->device, "ipu_init_channel_buffer error %d\n", retval); } if (mxc_fbi->alpha_chan_en) { retval = ipu_init_channel_buffer(mxc_fbi->ipu_ch, IPU_ALPHA_IN_BUFFER, IPU_PIX_FMT_GENERIC, fbi->var.xres, fbi->var.yres, fbi->var.xres, IPU_ROTATE_NONE, mxc_fbi->alpha_phy_addr1, mxc_fbi->alpha_phy_addr0, 0, 0); if (retval) { dev_err(fbi->device, "ipu_init_channel_buffer error %d\n", retval); return retval; } } return retval; } /* * Set framebuffer parameters and change the operating mode. * * @param info framebuffer information pointer */ static int mxcfb_set_par(struct fb_info *fbi) { int retval = 0; u32 mem_len, alpha_mem_len; ipu_di_signal_cfg_t sig_cfg; struct mxcfb_info *mxc_fbi = (struct mxcfb_info *)fbi->par; dev_dbg(fbi->device, "Reconfiguring framebuffer\n"); ipu_disable_irq(mxc_fbi->ipu_ch_irq); ipu_disable_channel(mxc_fbi->ipu_ch, true); ipu_uninit_channel(mxc_fbi->ipu_ch); ipu_clear_irq(mxc_fbi->ipu_ch_irq); mxcfb_set_fix(fbi); mem_len = fbi->var.yres_virtual * fbi->fix.line_length; if (!fbi->fix.smem_start || (mem_len > fbi->fix.smem_len)) { if (fbi->fix.smem_start) mxcfb_unmap_video_memory(fbi); if (mxcfb_map_video_memory(fbi) < 0) return -ENOMEM; } if (mxc_fbi->alpha_chan_en) { alpha_mem_len = fbi->var.xres * fbi->var.yres; if ((!mxc_fbi->alpha_phy_addr0 && !mxc_fbi->alpha_phy_addr1) || (alpha_mem_len > mxc_fbi->alpha_mem_len)) { if (mxc_fbi->alpha_phy_addr0) dma_free_coherent(fbi->device, mxc_fbi->alpha_mem_len, mxc_fbi->alpha_virt_addr0, mxc_fbi->alpha_phy_addr0); if (mxc_fbi->alpha_phy_addr1) dma_free_coherent(fbi->device, mxc_fbi->alpha_mem_len, mxc_fbi->alpha_virt_addr1, mxc_fbi->alpha_phy_addr1); mxc_fbi->alpha_virt_addr0 = dma_alloc_coherent(fbi->device, alpha_mem_len, &mxc_fbi->alpha_phy_addr0, GFP_DMA | GFP_KERNEL); mxc_fbi->alpha_virt_addr1 = dma_alloc_coherent(fbi->device, alpha_mem_len, &mxc_fbi->alpha_phy_addr1, GFP_DMA | GFP_KERNEL); if (mxc_fbi->alpha_virt_addr0 == NULL || mxc_fbi->alpha_virt_addr1 == NULL) { dev_err(fbi->device, "mxcfb: dma alloc for" " alpha buffer failed.\n"); if (mxc_fbi->alpha_virt_addr0) dma_free_coherent(fbi->device, mxc_fbi->alpha_mem_len, mxc_fbi->alpha_virt_addr0, mxc_fbi->alpha_phy_addr0); if (mxc_fbi->alpha_virt_addr1) dma_free_coherent(fbi->device, mxc_fbi->alpha_mem_len, mxc_fbi->alpha_virt_addr1, mxc_fbi->alpha_phy_addr1); return -ENOMEM; } mxc_fbi->alpha_mem_len = alpha_mem_len; } } _setup_disp_channel1(fbi); if (!mxc_fbi->overlay) { uint32_t out_pixel_fmt; memset(&sig_cfg, 0, sizeof(sig_cfg)); if (fbi->var.vmode & FB_VMODE_INTERLACED) { sig_cfg.interlaced = true; out_pixel_fmt = IPU_PIX_FMT_YUV444; } else { if (mxc_fbi->ipu_di_pix_fmt) out_pixel_fmt = mxc_fbi->ipu_di_pix_fmt; else out_pixel_fmt = IPU_PIX_FMT_RGB666; } if (fbi->var.vmode & FB_VMODE_ODD_FLD_FIRST) /* PAL */ sig_cfg.odd_field_first = true; if ((fbi->var.sync & FB_SYNC_EXT) || ext_clk_used) sig_cfg.ext_clk = true; if (fbi->var.sync & FB_SYNC_HOR_HIGH_ACT) sig_cfg.Hsync_pol = true; if (fbi->var.sync & FB_SYNC_VERT_HIGH_ACT) sig_cfg.Vsync_pol = true; if (!(fbi->var.sync & FB_SYNC_CLK_LAT_FALL)) sig_cfg.clk_pol = true; if (fbi->var.sync & FB_SYNC_DATA_INVERT) sig_cfg.data_pol = true; if (!(fbi->var.sync & FB_SYNC_OE_LOW_ACT)) sig_cfg.enable_pol = true; if (fbi->var.sync & FB_SYNC_CLK_IDLE_EN) sig_cfg.clkidle_en = true; dev_dbg(fbi->device, "pixclock = %ul Hz\n", (u32) (PICOS2KHZ(fbi->var.pixclock) * 1000UL)); if (ipu_init_sync_panel(mxc_fbi->ipu_di, (PICOS2KHZ(fbi->var.pixclock)) * 1000UL, fbi->var.xres, fbi->var.yres, out_pixel_fmt, fbi->var.left_margin, fbi->var.hsync_len, fbi->var.right_margin, fbi->var.upper_margin, fbi->var.vsync_len, fbi->var.lower_margin, 0, sig_cfg) != 0) { dev_err(fbi->device, "mxcfb: Error initializing panel.\n"); return -EINVAL; } fbi->mode = (struct fb_videomode *)fb_match_mode(&fbi->var, &fbi->modelist); ipu_disp_set_window_pos(mxc_fbi->ipu_ch, 0, 0); } retval = _setup_disp_channel2(fbi); if (retval) return retval; if (mxc_fbi->blank == FB_BLANK_UNBLANK) { ipu_enable_channel(mxc_fbi->ipu_ch); } return retval; } static int _swap_channels(struct fb_info *fbi, struct fb_info *fbi_to, bool both_on) { int retval, tmp; ipu_channel_t old_ch; struct mxcfb_info *mxc_fbi_from = (struct mxcfb_info *)fbi->par; struct mxcfb_info *mxc_fbi_to = (struct mxcfb_info *)fbi_to->par; if (both_on) { ipu_disable_channel(mxc_fbi_to->ipu_ch, true); ipu_uninit_channel(mxc_fbi_to->ipu_ch); } /* switch the mxc fbi parameters */ old_ch = mxc_fbi_from->ipu_ch; mxc_fbi_from->ipu_ch = mxc_fbi_to->ipu_ch; mxc_fbi_to->ipu_ch = old_ch; tmp = mxc_fbi_from->ipu_ch_irq; mxc_fbi_from->ipu_ch_irq = mxc_fbi_to->ipu_ch_irq; mxc_fbi_to->ipu_ch_irq = tmp; _setup_disp_channel1(fbi); retval = _setup_disp_channel2(fbi); if (retval) return retval; /* switch between dp and dc, disable old idmac, enable new idmac */ retval = ipu_swap_channel(old_ch, mxc_fbi_from->ipu_ch); ipu_uninit_channel(old_ch); if (both_on) { _setup_disp_channel1(fbi_to); retval = _setup_disp_channel2(fbi_to); if (retval) return retval; ipu_enable_channel(mxc_fbi_to->ipu_ch); } return retval; } static int swap_channels(struct fb_info *fbi) { int i; int swap_mode; ipu_channel_t ch_to; struct mxcfb_info *mxc_fbi_from = (struct mxcfb_info *)fbi->par; struct fb_info *fbi_to = NULL; struct mxcfb_info *mxc_fbi_to; /* what's the target channel? */ if (mxc_fbi_from->ipu_ch == MEM_BG_SYNC) ch_to = MEM_DC_SYNC; else ch_to = MEM_BG_SYNC; for (i = 0; i < num_registered_fb; i++) { mxc_fbi_to = (struct mxcfb_info *)mxcfb_info[i]->par; if (mxc_fbi_to->ipu_ch == ch_to) { fbi_to = mxcfb_info[i]; break; } } if (fbi_to == NULL) return -1; ipu_clear_irq(mxc_fbi_from->ipu_ch_irq); ipu_clear_irq(mxc_fbi_to->ipu_ch_irq); ipu_free_irq(mxc_fbi_from->ipu_ch_irq, fbi); ipu_free_irq(mxc_fbi_to->ipu_ch_irq, fbi_to); if (mxc_fbi_from->blank == FB_BLANK_UNBLANK) { if (mxc_fbi_to->blank == FB_BLANK_UNBLANK) swap_mode = BOTH_ON; else swap_mode = SRC_ON; } else { if (mxc_fbi_to->blank == FB_BLANK_UNBLANK) swap_mode = TGT_ON; else swap_mode = BOTH_OFF; } /* tvout di-1: for DC use UYVY, for DP use RGB */ if (mxc_fbi_from->ipu_di == 1 && ch_to == MEM_DC_SYNC) { fbi->var.bits_per_pixel = 16; fbi->var.nonstd = IPU_PIX_FMT_UYVY; } else if (mxc_fbi_from->ipu_di == 1 && ch_to == MEM_BG_SYNC) { fbi->var.nonstd = 0; } else if (mxc_fbi_from->ipu_di == 0 && ch_to == MEM_DC_SYNC) { fbi_to->var.nonstd = 0; } else if (mxc_fbi_from->ipu_di == 0 && ch_to == MEM_BG_SYNC) { fbi->var.bits_per_pixel = 16; fbi->var.nonstd = IPU_PIX_FMT_UYVY; } switch (swap_mode) { case BOTH_ON: /* disable target->switch src->enable target */ _swap_channels(fbi, fbi_to, true); break; case SRC_ON: /* just switch src */ _swap_channels(fbi, fbi_to, false); break; case TGT_ON: /* just switch target */ _swap_channels(fbi_to, fbi, false); break; case BOTH_OFF: /* switch directly, no more need to do */ mxc_fbi_to->ipu_ch = mxc_fbi_from->ipu_ch; mxc_fbi_from->ipu_ch = ch_to; i = mxc_fbi_from->ipu_ch_irq; mxc_fbi_from->ipu_ch_irq = mxc_fbi_to->ipu_ch_irq; mxc_fbi_to->ipu_ch_irq = i; break; default: break; } if (ipu_request_irq(mxc_fbi_from->ipu_ch_irq, mxcfb_irq_handler, 0, MXCFB_NAME, fbi) != 0) { dev_err(fbi->device, "Error registering irq %d\n", mxc_fbi_from->ipu_ch_irq); return -EBUSY; } if (ipu_request_irq(mxc_fbi_to->ipu_ch_irq, mxcfb_irq_handler, 0, MXCFB_NAME, fbi_to) != 0) { dev_err(fbi_to->device, "Error registering irq %d\n", mxc_fbi_to->ipu_ch_irq); return -EBUSY; } return 0; } /* * Check framebuffer variable parameters and adjust to valid values. * * @param var framebuffer variable parameters * * @param info framebuffer information pointer */ static int mxcfb_check_var(struct fb_var_screeninfo *var, struct fb_info *info) { u32 vtotal; u32 htotal; if (var->xres_virtual < var->xres) var->xres_virtual = var->xres; if (var->yres_virtual < var->yres) var->yres_virtual = var->yres; if ((var->bits_per_pixel != 32) && (var->bits_per_pixel != 24) && (var->bits_per_pixel != 16) && (var->bits_per_pixel != 8)) var->bits_per_pixel = default_bpp; switch (var->bits_per_pixel) { case 8: var->red.length = 3; var->red.offset = 5; var->red.msb_right = 0; var->green.length = 3; var->green.offset = 2; var->green.msb_right = 0; var->blue.length = 2; var->blue.offset = 0; var->blue.msb_right = 0; var->transp.length = 0; var->transp.offset = 0; var->transp.msb_right = 0; break; case 16: var->red.length = 5; var->red.offset = 11; var->red.msb_right = 0; var->green.length = 6; var->green.offset = 5; var->green.msb_right = 0; var->blue.length = 5; var->blue.offset = 0; var->blue.msb_right = 0; var->transp.length = 0; var->transp.offset = 0; var->transp.msb_right = 0; break; case 24: var->red.length = 8; var->red.offset = 16; var->red.msb_right = 0; var->green.length = 8; var->green.offset = 8; var->green.msb_right = 0; var->blue.length = 8; var->blue.offset = 0; var->blue.msb_right = 0; var->transp.length = 0; var->transp.offset = 0; var->transp.msb_right = 0; break; case 32: var->red.length = 8; var->red.offset = 16; var->red.msb_right = 0; var->green.length = 8; var->green.offset = 8; var->green.msb_right = 0; var->blue.length = 8; var->blue.offset = 0; var->blue.msb_right = 0; var->transp.length = 8; var->transp.offset = 24; var->transp.msb_right = 0; break; } if (var->pixclock < 1000) { htotal = var->xres + var->right_margin + var->hsync_len + var->left_margin; vtotal = var->yres + var->lower_margin + var->vsync_len + var->upper_margin; var->pixclock = (vtotal * htotal * 6UL) / 100UL; var->pixclock = KHZ2PICOS(var->pixclock); dev_dbg(info->device, "pixclock set for 60Hz refresh = %u ps\n", var->pixclock); } var->height = -1; var->width = -1; var->grayscale = 0; return 0; } static inline u_int _chan_to_field(u_int chan, struct fb_bitfield *bf) { chan &= 0xffff; chan >>= 16 - bf->length; return chan << bf->offset; } static int mxcfb_setcolreg(u_int regno, u_int red, u_int green, u_int blue, u_int trans, struct fb_info *fbi) { unsigned int val; int ret = 1; /* * If greyscale is true, then we convert the RGB value * to greyscale no matter what visual we are using. */ if (fbi->var.grayscale) red = green = blue = (19595 * red + 38470 * green + 7471 * blue) >> 16; switch (fbi->fix.visual) { case FB_VISUAL_TRUECOLOR: /* * 16-bit True Colour. We encode the RGB value * according to the RGB bitfield information. */ if (regno < 16) { u32 *pal = fbi->pseudo_palette; val = _chan_to_field(red, &fbi->var.red); val |= _chan_to_field(green, &fbi->var.green); val |= _chan_to_field(blue, &fbi->var.blue); pal[regno] = val; ret = 0; } break; case FB_VISUAL_STATIC_PSEUDOCOLOR: case FB_VISUAL_PSEUDOCOLOR: break; } return ret; } /* * Function to handle custom ioctls for MXC framebuffer. * * @param inode inode struct * * @param file file struct * * @param cmd Ioctl command to handle * * @param arg User pointer to command arguments * * @param fbi framebuffer information pointer */ static int mxcfb_ioctl(struct fb_info *fbi, unsigned int cmd, unsigned long arg) { int retval = 0; int __user *argp = (void __user *)arg; struct mxcfb_info *mxc_fbi = (struct mxcfb_info *)fbi->par; switch (cmd) { case MXCFB_SET_GBL_ALPHA: { struct mxcfb_gbl_alpha ga; if (copy_from_user(&ga, (void *)arg, sizeof(ga))) { retval = -EFAULT; break; } if (ipu_disp_set_global_alpha(mxc_fbi->ipu_ch, (bool)ga.enable, ga.alpha)) { retval = -EINVAL; break; } if (ga.enable) mxc_fbi->alpha_chan_en = false; if (ga.enable) dev_dbg(fbi->device, "Set global alpha of %s to %d\n", fbi->fix.id, ga.alpha); break; } case MXCFB_SET_LOC_ALPHA: { struct mxcfb_loc_alpha la; int i; char *video_plane_idstr = ""; if (copy_from_user(&la, (void *)arg, sizeof(la))) { retval = -EFAULT; break; } if (ipu_disp_set_global_alpha(mxc_fbi->ipu_ch, !(bool)la.enable, 0)) { retval = -EINVAL; break; } if (la.enable && !la.alpha_in_pixel) { mxc_fbi->alpha_chan_en = true; if (mxc_fbi->ipu_ch == MEM_FG_SYNC) video_plane_idstr = "DISP3 BG"; else if (mxc_fbi->ipu_ch == MEM_BG_SYNC) video_plane_idstr = "DISP3 FG"; for (i = 0; i < num_registered_fb; i++) { char *idstr = registered_fb[i]->fix.id; if (strcmp(idstr, video_plane_idstr) == 0) { ((struct mxcfb_info *)(registered_fb[i]->par))->alpha_chan_en = false; break; } } } else mxc_fbi->alpha_chan_en = false; mxcfb_set_par(fbi); la.alpha_phy_addr0 = mxc_fbi->alpha_phy_addr0; la.alpha_phy_addr1 = mxc_fbi->alpha_phy_addr1; if (copy_to_user((void *)arg, &la, sizeof(la))) { retval = -EFAULT; break; } if (la.enable) dev_dbg(fbi->device, "Enable DP local alpha for %s\n", fbi->fix.id); break; } case MXCFB_SET_LOC_ALP_BUF: { unsigned long base; uint32_t ipu_alp_ch_irq; if (!(((mxc_fbi->ipu_ch == MEM_FG_SYNC) || (mxc_fbi->ipu_ch == MEM_BG_SYNC)) && (mxc_fbi->alpha_chan_en))) { dev_err(fbi->device, "Should use background or overlay " "framebuffer to set the alpha buffer " "number\n"); return -EINVAL; } if (get_user(base, argp)) return -EFAULT; if (base != mxc_fbi->alpha_phy_addr0 && base != mxc_fbi->alpha_phy_addr1) { dev_err(fbi->device, "Wrong alpha buffer physical address " "%lu\n", base); return -EINVAL; } if (mxc_fbi->ipu_ch == MEM_FG_SYNC) ipu_alp_ch_irq = IPU_IRQ_FG_ALPHA_SYNC_EOF; else ipu_alp_ch_irq = IPU_IRQ_BG_ALPHA_SYNC_EOF; down(&mxc_fbi->alpha_flip_sem); mxc_fbi->cur_ipu_alpha_buf = !mxc_fbi->cur_ipu_alpha_buf; if (ipu_update_channel_buffer(mxc_fbi->ipu_ch, IPU_ALPHA_IN_BUFFER, mxc_fbi-> cur_ipu_alpha_buf, base) == 0) { ipu_select_buffer(mxc_fbi->ipu_ch, IPU_ALPHA_IN_BUFFER, mxc_fbi->cur_ipu_alpha_buf); ipu_clear_irq(ipu_alp_ch_irq); ipu_enable_irq(ipu_alp_ch_irq); } else { dev_err(fbi->device, "Error updating %s SDC alpha buf %d " "to address=0x%08lX\n", fbi->fix.id, mxc_fbi->cur_ipu_alpha_buf, base); } break; } case MXCFB_SET_CLR_KEY: { struct mxcfb_color_key key; if (copy_from_user(&key, (void *)arg, sizeof(key))) { retval = -EFAULT; break; } retval = ipu_disp_set_color_key(mxc_fbi->ipu_ch, key.enable, key.color_key); dev_dbg(fbi->device, "Set color key to 0x%08X\n", key.color_key); break; } case MXCFB_SET_GAMMA: { struct mxcfb_gamma gamma; if (copy_from_user(&gamma, (void *)arg, sizeof(gamma))) { retval = -EFAULT; break; } retval = ipu_disp_set_gamma_correction(mxc_fbi->ipu_ch, gamma.enable, gamma.constk, gamma.slopek); break; } case MXCFB_WAIT_FOR_VSYNC: { if (mxc_fbi->ipu_ch == MEM_FG_SYNC) { struct mxcfb_info *bg_mxcfbi = NULL; int i; for (i = 0; i < num_registered_fb; i++) { bg_mxcfbi = ((struct mxcfb_info *)(registered_fb[i]->par)); if (bg_mxcfbi->ipu_ch == MEM_BG_SYNC) break; } if (bg_mxcfbi->blank != FB_BLANK_UNBLANK) { retval = -EINVAL; break; } } if (mxc_fbi->blank != FB_BLANK_UNBLANK) { retval = -EINVAL; break; } down(&mxc_fbi->flip_sem); init_completion(&mxc_fbi->vsync_complete); ipu_clear_irq(mxc_fbi->ipu_ch_irq); ipu_enable_irq(mxc_fbi->ipu_ch_irq); retval = wait_for_completion_interruptible_timeout( &mxc_fbi->vsync_complete, 1 * HZ); if (retval == 0) { dev_err(fbi->device, "MXCFB_WAIT_FOR_VSYNC: timeout %d\n", retval); retval = -ETIME; } else if (retval > 0) { retval = 0; } break; } case FBIO_ALLOC: { int size; struct mxcfb_alloc_list *mem; mem = kzalloc(sizeof(*mem), GFP_KERNEL); if (mem == NULL) return -ENOMEM; if (get_user(size, argp)) return -EFAULT; mem->size = PAGE_ALIGN(size); mem->cpu_addr = dma_alloc_coherent(fbi->device, size, &mem->phy_addr, GFP_DMA); if (mem->cpu_addr == NULL) { kfree(mem); return -ENOMEM; } list_add(&mem->list, &fb_alloc_list); dev_dbg(fbi->device, "allocated %d bytes @ 0x%08X\n", mem->size, mem->phy_addr); if (put_user(mem->phy_addr, argp)) return -EFAULT; break; } case FBIO_FREE: { unsigned long offset; struct mxcfb_alloc_list *mem; if (get_user(offset, argp)) return -EFAULT; retval = -EINVAL; list_for_each_entry(mem, &fb_alloc_list, list) { if (mem->phy_addr == offset) { list_del(&mem->list); dma_free_coherent(fbi->device, mem->size, mem->cpu_addr, mem->phy_addr); kfree(mem); retval = 0; break; } } break; } case MXCFB_SET_OVERLAY_POS: { struct mxcfb_pos pos; struct fb_info *bg_fbi = NULL; struct mxcfb_info *bg_mxcfbi = NULL; int i; if (mxc_fbi->ipu_ch != MEM_FG_SYNC) { dev_err(fbi->device, "Should use the overlay " "framebuffer to set the position of " "the overlay window\n"); retval = -EINVAL; break; } if (copy_from_user(&pos, (void *)arg, sizeof(pos))) { retval = -EFAULT; break; } for (i = 0; i < num_registered_fb; i++) { bg_mxcfbi = ((struct mxcfb_info *)(registered_fb[i]->par)); if (bg_mxcfbi->ipu_ch == MEM_BG_SYNC) { bg_fbi = registered_fb[i]; break; } } if (bg_fbi == NULL) { dev_err(fbi->device, "Cannot find the " "background framebuffer\n"); retval = -ENOENT; break; } if (fbi->var.xres + pos.x > bg_fbi->var.xres) { if (bg_fbi->var.xres < fbi->var.xres) pos.x = 0; else pos.x = bg_fbi->var.xres - fbi->var.xres; } if (fbi->var.yres + pos.y > bg_fbi->var.yres) { if (bg_fbi->var.yres < fbi->var.yres) pos.y = 0; else pos.y = bg_fbi->var.yres - fbi->var.yres; } retval = ipu_disp_set_window_pos(mxc_fbi->ipu_ch, pos.x, pos.y); if (copy_to_user((void *)arg, &pos, sizeof(pos))) { retval = -EFAULT; break; } break; } case MXCFB_GET_FB_IPU_CHAN: { struct mxcfb_info *mxc_fbi = (struct mxcfb_info *)fbi->par; if (put_user(mxc_fbi->ipu_ch, argp)) return -EFAULT; break; } default: retval = -EINVAL; } return retval; } /* * mxcfb_blank(): * Blank the display. */ static int mxcfb_blank(int blank, struct fb_info *info) { struct mxcfb_info *mxc_fbi = (struct mxcfb_info *)info->par; dev_dbg(info->device, "blank = %d\n", blank); if (mxc_fbi->blank == blank) return 0; mxc_fbi->blank = blank; switch (blank) { case FB_BLANK_POWERDOWN: case FB_BLANK_VSYNC_SUSPEND: case FB_BLANK_HSYNC_SUSPEND: case FB_BLANK_NORMAL: ipu_disable_channel(mxc_fbi->ipu_ch, true); ipu_uninit_channel(mxc_fbi->ipu_ch); break; case FB_BLANK_UNBLANK: mxcfb_set_par(info); break; } return 0; } /* * Pan or Wrap the Display * * This call looks only at xoffset, yoffset and the FB_VMODE_YWRAP flag * * @param var Variable screen buffer information * @param info Framebuffer information pointer */ static int mxcfb_pan_display(struct fb_var_screeninfo *var, struct fb_info *info) { struct mxcfb_info *mxc_fbi = (struct mxcfb_info *)info->par, *mxc_graphic_fbi = NULL; u_int y_bottom; unsigned long base, active_alpha_phy_addr = 0; bool loc_alpha_en = false; int i = 0; if (var->xoffset > 0) { dev_dbg(info->device, "x panning not supported\n"); return -EINVAL; } if ((info->var.xoffset == var->xoffset) && (info->var.yoffset == var->yoffset)) return 0; /* No change, do nothing */ y_bottom = var->yoffset; if (!(var->vmode & FB_VMODE_YWRAP)) y_bottom += var->yres; if (y_bottom > info->var.yres_virtual) return -EINVAL; base = (var->yoffset * var->xres_virtual + var->xoffset); base *= (var->bits_per_pixel) / 8; base += info->fix.smem_start; dev_dbg(info->device, "Updating SDC %s buf %d address=0x%08lX\n", info->fix.id, mxc_fbi->cur_ipu_buf, base); /* Check if DP local alpha is enabled and find the graphic fb */ if (mxc_fbi->ipu_ch == MEM_BG_SYNC || mxc_fbi->ipu_ch == MEM_FG_SYNC) { for (i = 0; i < num_registered_fb; i++) { char *idstr = registered_fb[i]->fix.id; if ((strcmp(idstr, "DISP3 BG") == 0 || strcmp(idstr, "DISP3 FG") == 0) && ((struct mxcfb_info *) (registered_fb[i]->par))->alpha_chan_en) { loc_alpha_en = true; mxc_graphic_fbi = (struct mxcfb_info *) (registered_fb[i]->par); active_alpha_phy_addr = mxc_fbi->cur_ipu_buf ? mxc_graphic_fbi->alpha_phy_addr1 : mxc_graphic_fbi->alpha_phy_addr0; dev_dbg(info->device, "Updating SDC graphic " "buf %d address=0x%08lX\n", mxc_fbi->cur_ipu_buf, active_alpha_phy_addr); break; } } } down(&mxc_fbi->flip_sem); init_completion(&mxc_fbi->vsync_complete); mxc_fbi->cur_ipu_buf = !mxc_fbi->cur_ipu_buf; if (ipu_update_channel_buffer(mxc_fbi->ipu_ch, IPU_INPUT_BUFFER, mxc_fbi->cur_ipu_buf, base) == 0) { /* Update the DP local alpha buffer only for graphic plane */ if (loc_alpha_en && mxc_graphic_fbi == mxc_fbi && ipu_update_channel_buffer(mxc_graphic_fbi->ipu_ch, IPU_ALPHA_IN_BUFFER, mxc_fbi->cur_ipu_buf, active_alpha_phy_addr) == 0) { ipu_select_buffer(mxc_graphic_fbi->ipu_ch, IPU_ALPHA_IN_BUFFER, mxc_fbi->cur_ipu_buf); } ipu_select_buffer(mxc_fbi->ipu_ch, IPU_INPUT_BUFFER, mxc_fbi->cur_ipu_buf); ipu_clear_irq(mxc_fbi->ipu_ch_irq); ipu_enable_irq(mxc_fbi->ipu_ch_irq); } else { dev_err(info->device, "Error updating SDC buf %d to address=0x%08lX\n", mxc_fbi->cur_ipu_buf, base); } dev_dbg(info->device, "Update complete\n"); info->var.xoffset = var->xoffset; info->var.yoffset = var->yoffset; if (var->vmode & FB_VMODE_YWRAP) info->var.vmode |= FB_VMODE_YWRAP; else info->var.vmode &= ~FB_VMODE_YWRAP; return 0; } /* * Function to handle custom mmap for MXC framebuffer. * * @param fbi framebuffer information pointer * * @param vma Pointer to vm_area_struct */ static int mxcfb_mmap(struct fb_info *fbi, struct vm_area_struct *vma) { bool found = false; u32 len; unsigned long offset = vma->vm_pgoff << PAGE_SHIFT; struct mxcfb_alloc_list *mem; struct mxcfb_info *mxc_fbi = (struct mxcfb_info *)fbi->par; if (offset < fbi->fix.smem_len) { /* mapping framebuffer memory */ len = fbi->fix.smem_len - offset; vma->vm_pgoff = (fbi->fix.smem_start + offset) >> PAGE_SHIFT; } else if ((vma->vm_pgoff == (mxc_fbi->alpha_phy_addr0 >> PAGE_SHIFT)) || (vma->vm_pgoff == (mxc_fbi->alpha_phy_addr1 >> PAGE_SHIFT))) { len = mxc_fbi->alpha_mem_len; } else { list_for_each_entry(mem, &fb_alloc_list, list) { if (offset == mem->phy_addr) { found = true; len = mem->size; break; } } if (!found) return -EINVAL; } len = PAGE_ALIGN(len); if (vma->vm_end - vma->vm_start > len) return -EINVAL; /* make buffers bufferable */ vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot); vma->vm_flags |= VM_IO | VM_RESERVED; if (remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, vma->vm_end - vma->vm_start, vma->vm_page_prot)) { dev_dbg(fbi->device, "mmap remap_pfn_range failed\n"); return -ENOBUFS; } return 0; } /*! * This structure contains the pointers to the control functions that are * invoked by the core framebuffer driver to perform operations like * blitting, rectangle filling, copy regions and cursor definition. */ static struct fb_ops mxcfb_ops = { .owner = THIS_MODULE, .fb_set_par = mxcfb_set_par, .fb_check_var = mxcfb_check_var, .fb_setcolreg = mxcfb_setcolreg, .fb_pan_display = mxcfb_pan_display, .fb_ioctl = mxcfb_ioctl, .fb_mmap = mxcfb_mmap, .fb_fillrect = cfb_fillrect, .fb_copyarea = cfb_copyarea, .fb_imageblit = cfb_imageblit, .fb_blank = mxcfb_blank, }; static irqreturn_t mxcfb_irq_handler(int irq, void *dev_id) { struct fb_info *fbi = dev_id; struct mxcfb_info *mxc_fbi = fbi->par; complete(&mxc_fbi->vsync_complete); up(&mxc_fbi->flip_sem); ipu_disable_irq(irq); return IRQ_HANDLED; } static irqreturn_t mxcfb_alpha_irq_handler(int irq, void *dev_id) { struct fb_info *fbi = dev_id; struct mxcfb_info *mxc_fbi = fbi->par; up(&mxc_fbi->alpha_flip_sem); ipu_disable_irq(irq); return IRQ_HANDLED; } /* * Suspends the framebuffer and blanks the screen. Power management support */ static int mxcfb_suspend(struct platform_device *pdev, pm_message_t state) { struct fb_info *fbi = platform_get_drvdata(pdev); struct mxcfb_info *mxc_fbi = (struct mxcfb_info *)fbi->par; int saved_blank; #ifdef CONFIG_FB_MXC_LOW_PWR_DISPLAY void *fbmem; #endif acquire_console_sem(); fb_set_suspend(fbi, 1); saved_blank = mxc_fbi->blank; mxcfb_blank(FB_BLANK_POWERDOWN, fbi); mxc_fbi->blank = saved_blank; release_console_sem(); return 0; } /* * Resumes the framebuffer and unblanks the screen. Power management support */ static int mxcfb_resume(struct platform_device *pdev) { struct fb_info *fbi = platform_get_drvdata(pdev); struct mxcfb_info *mxc_fbi = (struct mxcfb_info *)fbi->par; int saved_blank; acquire_console_sem(); saved_blank = mxc_fbi->blank; mxc_fbi->blank = FB_BLANK_POWERDOWN; mxcfb_blank(saved_blank, fbi); fb_set_suspend(fbi, 0); release_console_sem(); return 0; } /* * Main framebuffer functions */ /*! * Allocates the DRAM memory for the frame buffer. This buffer is remapped * into a non-cached, non-buffered, memory region to allow palette and pixel * writes to occur without flushing the cache. Once this area is remapped, * all virtual memory access to the video memory should occur at the new region. * * @param fbi framebuffer information pointer * * @return Error code indicating success or failure */ static int mxcfb_map_video_memory(struct fb_info *fbi) { if (fbi->fix.smem_len < fbi->var.yres_virtual * fbi->fix.line_length) fbi->fix.smem_len = fbi->var.yres_virtual * fbi->fix.line_length; fbi->screen_base = dma_alloc_writecombine(fbi->device, fbi->fix.smem_len, (dma_addr_t *)&fbi->fix.smem_start, GFP_DMA); if (fbi->screen_base == 0) { dev_err(fbi->device, "Unable to allocate framebuffer memory\n"); fbi->fix.smem_len = 0; fbi->fix.smem_start = 0; return -EBUSY; } dev_dbg(fbi->device, "allocated fb @ paddr=0x%08X, size=%d.\n", (uint32_t) fbi->fix.smem_start, fbi->fix.smem_len); fbi->screen_size = fbi->fix.smem_len; /* Clear the screen */ memset((char *)fbi->screen_base, 0, fbi->fix.smem_len); return 0; } /*! * De-allocates the DRAM memory for the frame buffer. * * @param fbi framebuffer information pointer * * @return Error code indicating success or failure */ static int mxcfb_unmap_video_memory(struct fb_info *fbi) { dma_free_writecombine(fbi->device, fbi->fix.smem_len, fbi->screen_base, fbi->fix.smem_start); fbi->screen_base = 0; fbi->fix.smem_start = 0; fbi->fix.smem_len = 0; return 0; } /*! * Initializes the framebuffer information pointer. After allocating * sufficient memory for the framebuffer structure, the fields are * filled with custom information passed in from the configurable * structures. This includes information such as bits per pixel, * color maps, screen width/height and RGBA offsets. * * @return Framebuffer structure initialized with our information */ static struct fb_info *mxcfb_init_fbinfo(struct device *dev, struct fb_ops *ops) { struct fb_info *fbi; struct mxcfb_info *mxcfbi; /* * Allocate sufficient memory for the fb structure */ fbi = framebuffer_alloc(sizeof(struct mxcfb_info), dev); if (!fbi) return NULL; mxcfbi = (struct mxcfb_info *)fbi->par; fbi->var.activate = FB_ACTIVATE_NOW; fbi->fbops = ops; fbi->flags = FBINFO_FLAG_DEFAULT; fbi->pseudo_palette = mxcfbi->pseudo_palette; /* * Allocate colormap */ fb_alloc_cmap(&fbi->cmap, 16, 0); return fbi; } static ssize_t show_disp_chan(struct device *dev, struct device_attribute *attr, char *buf) { struct fb_info *info = dev_get_drvdata(dev); struct mxcfb_info *mxcfbi = (struct mxcfb_info *)info->par; if (mxcfbi->ipu_ch == MEM_BG_SYNC) return sprintf(buf, "2-layer-fb-bg\n"); else if (mxcfbi->ipu_ch == MEM_FG_SYNC) return sprintf(buf, "2-layer-fb-fg\n"); else if (mxcfbi->ipu_ch == MEM_DC_SYNC) return sprintf(buf, "1-layer-fb\n"); else return sprintf(buf, "err: no display chan\n"); } static ssize_t swap_disp_chan(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { struct fb_info *info = dev_get_drvdata(dev); struct mxcfb_info *mxcfbi = (struct mxcfb_info *)info->par; struct mxcfb_info *fg_mxcfbi = NULL; /* swap only happen between DP-BG and DC, while DP-FG disable */ if (((mxcfbi->ipu_ch == MEM_BG_SYNC) && (strstr(buf, "1-layer-fb") != NULL)) || ((mxcfbi->ipu_ch == MEM_DC_SYNC) && (strstr(buf, "2-layer-fb-bg") != NULL))) { int i; for (i = 0; i < num_registered_fb; i++) { fg_mxcfbi = (struct mxcfb_info *)mxcfb_info[i]->par; if (fg_mxcfbi->ipu_ch == MEM_FG_SYNC) break; else fg_mxcfbi = NULL; } if (!fg_mxcfbi || fg_mxcfbi->blank == FB_BLANK_UNBLANK) { dev_err(dev, "Can not switch while fb2(fb-fg) is on.\n"); return count; } if (swap_channels(info) < 0) dev_err(dev, "Swap display channel failed.\n"); } return count; } DEVICE_ATTR(fsl_disp_property, 644, show_disp_chan, swap_disp_chan); /*! * Probe routine for the framebuffer driver. It is called during the * driver binding process. The following functions are performed in * this routine: Framebuffer initialization, Memory allocation and * mapping, Framebuffer registration, IPU initialization. * * @return Appropriate error code to the kernel common code */ static int mxcfb_probe(struct platform_device *pdev) { struct fb_info *fbi; struct mxcfb_info *mxcfbi; struct mxc_fb_platform_data *plat_data = pdev->dev.platform_data; struct resource *res; int ret = 0; /* * Initialize FB structures */ fbi = mxcfb_init_fbinfo(&pdev->dev, &mxcfb_ops); if (!fbi) { ret = -ENOMEM; goto err0; } mxcfbi = (struct mxcfb_info *)fbi->par; if (!g_dp_in_use) { mxcfbi->ipu_ch_irq = IPU_IRQ_BG_SYNC_EOF; mxcfbi->ipu_ch = MEM_BG_SYNC; mxcfbi->blank = FB_BLANK_UNBLANK; } else { mxcfbi->ipu_ch_irq = IPU_IRQ_DC_SYNC_EOF; mxcfbi->ipu_ch = MEM_DC_SYNC; mxcfbi->blank = FB_BLANK_POWERDOWN; } mxcfbi->ipu_di = pdev->id; if (pdev->id == 0) { ipu_disp_set_global_alpha(mxcfbi->ipu_ch, true, 0x80); ipu_disp_set_color_key(mxcfbi->ipu_ch, false, 0); strcpy(fbi->fix.id, "DISP3 BG"); if (!g_dp_in_use) if (ipu_request_irq(IPU_IRQ_BG_ALPHA_SYNC_EOF, mxcfb_alpha_irq_handler, 0, MXCFB_NAME, fbi) != 0) { dev_err(&pdev->dev, "Error registering BG " "alpha irq handler.\n"); ret = -EBUSY; goto err1; } g_dp_in_use = true; } else if (pdev->id == 1) { strcpy(fbi->fix.id, "DISP3 BG - DI1"); if (!g_dp_in_use) if (ipu_request_irq(IPU_IRQ_BG_ALPHA_SYNC_EOF, mxcfb_alpha_irq_handler, 0, MXCFB_NAME, fbi) != 0) { dev_err(&pdev->dev, "Error registering BG " "alpha irq handler.\n"); ret = -EBUSY; goto err1; } g_dp_in_use = true; } else if (pdev->id == 2) { /* Overlay */ mxcfbi->ipu_ch_irq = IPU_IRQ_FG_SYNC_EOF; mxcfbi->ipu_ch = MEM_FG_SYNC; mxcfbi->ipu_di = -1; mxcfbi->overlay = true; mxcfbi->blank = FB_BLANK_POWERDOWN; strcpy(fbi->fix.id, "DISP3 FG"); if (ipu_request_irq(IPU_IRQ_FG_ALPHA_SYNC_EOF, mxcfb_alpha_irq_handler, 0, MXCFB_NAME, fbi) != 0) { dev_err(&pdev->dev, "Error registering FG alpha irq " "handler.\n"); ret = -EBUSY; goto err1; } } mxcfb_info[pdev->id] = fbi; if (ipu_request_irq(mxcfbi->ipu_ch_irq, mxcfb_irq_handler, 0, MXCFB_NAME, fbi) != 0) { dev_err(&pdev->dev, "Error registering BG irq handler.\n"); ret = -EBUSY; goto err1; } ipu_disable_irq(mxcfbi->ipu_ch_irq); res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (res) { fbi->fix.smem_len = res->end - res->start + 1; fbi->fix.smem_start = res->start; fbi->screen_base = ioremap(fbi->fix.smem_start, fbi->fix.smem_len); } /* Need dummy values until real panel is configured */ fbi->var.xres = 240; fbi->var.yres = 320; if (!fb_mode && plat_data && plat_data->mode_str) fb_find_mode(&fbi->var, fbi, plat_data->mode_str, NULL, 0, NULL, default_bpp); if (fb_mode) fb_find_mode(&fbi->var, fbi, fb_mode, NULL, 0, NULL, default_bpp); if (plat_data) { mxcfbi->ipu_di_pix_fmt = plat_data->interface_pix_fmt; if (!fb_mode && plat_data->mode) fb_videomode_to_var(&fbi->var, plat_data->mode); } mxcfb_check_var(&fbi->var, fbi); /* Default Y virtual size is 2x panel size */ fbi->var.yres_virtual = fbi->var.yres * 2; mxcfb_set_fix(fbi); /* alocate fb first */ if (!res) if (mxcfb_map_video_memory(fbi) < 0) return -ENOMEM; ret = register_framebuffer(fbi); if (ret < 0) goto err2; platform_set_drvdata(pdev, fbi); ret = device_create_file(fbi->dev, &dev_attr_fsl_disp_property); if (ret) dev_err(&pdev->dev, "Error %d on creating file\n", ret); return 0; err2: ipu_free_irq(mxcfbi->ipu_ch_irq, fbi); err1: fb_dealloc_cmap(&fbi->cmap); framebuffer_release(fbi); err0: return ret; } static int mxcfb_remove(struct platform_device *pdev) { struct fb_info *fbi = platform_get_drvdata(pdev); struct mxcfb_info *mxc_fbi = fbi->par; if (!fbi) return 0; mxcfb_blank(FB_BLANK_POWERDOWN, fbi); ipu_free_irq(mxc_fbi->ipu_ch_irq, fbi); mxcfb_unmap_video_memory(fbi); if (&fbi->cmap) fb_dealloc_cmap(&fbi->cmap); unregister_framebuffer(fbi); framebuffer_release(fbi); return 0; } /*! * This structure contains pointers to the power management callback functions. */ static struct platform_driver mxcfb_driver = { .driver = { .name = MXCFB_NAME, }, .probe = mxcfb_probe, .remove = mxcfb_remove, .suspend = mxcfb_suspend, .resume = mxcfb_resume, }; /* * Parse user specified options (`video=trident:') * example: * video=trident:800x600,bpp=16,noaccel */ int mxcfb_setup(char *options) { char *opt; if (!options || !*options) return 0; while ((opt = strsep(&options, ",")) != NULL) { if (!*opt) continue; if (!strncmp(opt, "ext_clk", 7)) { ext_clk_used = true; continue; } else ext_clk_used = false; if (!strncmp(opt, "bpp=", 4)) default_bpp = simple_strtoul(opt + 4, NULL, 0); else fb_mode = opt; } return 0; } /*! * Main entry function for the framebuffer. The function registers the power * management callback functions with the kernel and also registers the MXCFB * callback functions with the core Linux framebuffer driver \b fbmem.c * * @return Error code indicating success or failure */ int __init mxcfb_init(void) { int ret = 0; #ifndef MODULE char *option = NULL; #endif #ifndef MODULE if (fb_get_options("mxcfb", &option)) return -ENODEV; mxcfb_setup(option); #endif ret = platform_driver_register(&mxcfb_driver); return ret; } void mxcfb_exit(void) { platform_driver_unregister(&mxcfb_driver); } module_init(mxcfb_init); module_exit(mxcfb_exit); MODULE_AUTHOR("Freescale Semiconductor, Inc."); MODULE_DESCRIPTION("MXC framebuffer driver"); MODULE_LICENSE("GPL"); MODULE_SUPPORTED_DEVICE("fb");