mirror of
https://github.com/genesi/linux-legacy.git
synced 2026-09-23 07:05:27 +00:00
This is i.MX BSP 5.0.0 release ported to 2.6.31 Signed-off-by: Rob Herring <r.herring@freescale.com> Signed-off-by: Alan Tull <r80115@freescale.com> Signed-off-by: Xinyu Chen <xinyu.chen@freescale.com>
1159 lines
29 KiB
C
1159 lines
29 KiB
C
/*
|
|
* Copyright 2004-2009 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
|
|
*/
|
|
|
|
/*!
|
|
* @file mxcfb_epson.c
|
|
*
|
|
* @brief MXC Frame buffer driver for ADC
|
|
*
|
|
* @ingroup Framebuffer
|
|
*/
|
|
|
|
/*!
|
|
* Include files
|
|
*/
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/sched.h>
|
|
#include <linux/errno.h>
|
|
#include <linux/string.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/fb.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/init.h>
|
|
#include <linux/ioport.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/dma-mapping.h>
|
|
#include <mach/hardware.h>
|
|
#include <asm/io.h>
|
|
#include <asm/mach-types.h>
|
|
#include <asm/uaccess.h>
|
|
#include <mach/ipu.h>
|
|
#include <mach/mxcfb.h>
|
|
|
|
#define PARTIAL_REFRESH
|
|
#define MXCFB_REFRESH_DEFAULT MXCFB_REFRESH_PARTIAL
|
|
/*
|
|
* Driver name
|
|
*/
|
|
#define MXCFB_NAME "MXCFB_EPSON"
|
|
|
|
#define MXCFB_SCREEN_TOP_OFFSET 0
|
|
#define MXCFB_SCREEN_LEFT_OFFSET 2
|
|
#define MXCFB_SCREEN_WIDTH 176
|
|
#define MXCFB_SCREEN_HEIGHT 220
|
|
|
|
/*!
|
|
* Enum defining Epson panel commands.
|
|
*/
|
|
enum {
|
|
DISON = 0xAF,
|
|
DISOFF = 0xAE,
|
|
DISCTL = 0xCA,
|
|
SD_CSET = 0x15,
|
|
SD_PSET = 0x75,
|
|
DATCTL = 0xBC,
|
|
SLPIN = 0x95,
|
|
SLPOUT = 0x94,
|
|
DISNOR = 0xA6,
|
|
RAMWR = 0x5C,
|
|
VOLCTR = 0xC6,
|
|
GCP16 = 0xCC,
|
|
GCP64 = 0xCB,
|
|
};
|
|
|
|
struct mxcfb_info {
|
|
int open_count;
|
|
int blank;
|
|
uint32_t disp_num;
|
|
|
|
u32 pseudo_palette[16];
|
|
|
|
int32_t cur_update_mode;
|
|
dma_addr_t alloc_start_paddr;
|
|
void *alloc_start_vaddr;
|
|
u32 alloc_size;
|
|
uint32_t snoop_window_size;
|
|
};
|
|
|
|
struct mxcfb_data {
|
|
struct fb_info *fbi;
|
|
volatile int32_t vsync_flag;
|
|
wait_queue_head_t vsync_wq;
|
|
wait_queue_head_t suspend_wq;
|
|
bool suspended;
|
|
};
|
|
|
|
static struct mxcfb_data mxcfb_drv_data;
|
|
static unsigned long default_bpp = 16;
|
|
|
|
void slcd_gpio_config(void);
|
|
extern void gpio_lcd_active(void);
|
|
static int mxcfb_blank(int blank, struct fb_info *fbi);
|
|
|
|
static uint32_t bpp_to_pixfmt(int bpp)
|
|
{
|
|
uint32_t pixfmt = 0;
|
|
switch (bpp) {
|
|
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;
|
|
}
|
|
|
|
/*!
|
|
* This function sets display region in the Epson panel
|
|
*
|
|
* @param disp display panel to config
|
|
* @param x1 x-coordinate of one vertex.
|
|
* @param x2 x-coordinate of second vertex.
|
|
* @param y1 y-coordinate of one vertex.
|
|
* @param y2 y-coordinate of second vertex.
|
|
*/
|
|
void set_panel_region(int disp, uint32_t x1, uint32_t x2,
|
|
uint32_t y1, uint32_t y2)
|
|
{
|
|
uint32_t param[8];
|
|
|
|
memset(param, 0, sizeof(uint32_t) * 8);
|
|
param[0] = x1;
|
|
param[2] = x2;
|
|
param[4] = y1;
|
|
param[6] = y2;
|
|
|
|
// SD_CSET
|
|
ipu_adc_write_cmd(disp, CMD, SD_CSET, param, 4);
|
|
// SD_PSET
|
|
|
|
ipu_adc_write_cmd(disp, CMD, SD_PSET, &(param[4]), 4);
|
|
}
|
|
|
|
/*!
|
|
* Function to create and initiate template command buffer for ADC. This
|
|
* template will be written to Panel memory.
|
|
*/
|
|
static void init_channel_template(int disp)
|
|
{
|
|
/* template command buffer for ADC is 32 */
|
|
uint32_t tempCmd[TEMPLATE_BUF_SIZE];
|
|
uint32_t i = 0;
|
|
|
|
memset(tempCmd, 0, sizeof(uint32_t) * TEMPLATE_BUF_SIZE);
|
|
/* setup update display region */
|
|
/* whole the screen during init */
|
|
/*WRITE Y COORDINATE CMND */
|
|
tempCmd[i++] = ipu_adc_template_gen(WR_CMND, 0, SINGLE_STEP, SD_PSET);
|
|
/*WRITE Y START ADDRESS CMND LSB[22:8] */
|
|
tempCmd[i++] = ipu_adc_template_gen(WR_YADDR, 1, SINGLE_STEP, 0x01);
|
|
/*WRITE Y START ADDRESS CMND MSB[22:16] */
|
|
tempCmd[i++] = ipu_adc_template_gen(WR_YADDR, 1, SINGLE_STEP, 0x09);
|
|
/*WRITE Y STOP ADDRESS CMND LSB */
|
|
tempCmd[i++] = ipu_adc_template_gen(WR_CMND, 1, SINGLE_STEP,
|
|
MXCFB_SCREEN_HEIGHT - 1);
|
|
/*WRITE Y STOP ADDRESS CMND MSB */
|
|
tempCmd[i++] = ipu_adc_template_gen(WR_CMND, 1, SINGLE_STEP, 0);
|
|
/*WRITE X COORDINATE CMND */
|
|
tempCmd[i++] = ipu_adc_template_gen(WR_CMND, 0, SINGLE_STEP, SD_CSET);
|
|
/*WRITE X ADDRESS CMND LSB[7:0] */
|
|
tempCmd[i++] = ipu_adc_template_gen(WR_XADDR, 1, SINGLE_STEP, 0x01);
|
|
/*WRITE X ADDRESS CMND MSB[22:8] */
|
|
tempCmd[i++] = ipu_adc_template_gen(WR_CMND, 1, SINGLE_STEP, 0);
|
|
/*WRITE X STOP ADDRESS CMND LSB */
|
|
tempCmd[i++] = ipu_adc_template_gen(WR_CMND, 1, SINGLE_STEP,
|
|
MXCFB_SCREEN_WIDTH + 1);
|
|
/*WRITE X STOP ADDRESS CMND MSB */
|
|
tempCmd[i++] = ipu_adc_template_gen(WR_CMND, 1, SINGLE_STEP, 0);
|
|
/*WRITE MEMORY CMND MSB */
|
|
tempCmd[i++] = ipu_adc_template_gen(WR_CMND, 0, SINGLE_STEP, RAMWR);
|
|
/*WRITE DATA CMND and STP */
|
|
tempCmd[i++] = ipu_adc_template_gen(WR_DATA, 1, STOP, 0);
|
|
|
|
ipu_adc_write_template(disp, tempCmd, true);
|
|
}
|
|
|
|
/*!
|
|
* Function to initialize the panel. First it resets the panel and then
|
|
* initilizes panel.
|
|
*/
|
|
static void _init_panel(int disp)
|
|
{
|
|
uint32_t cmd_param;
|
|
uint32_t i;
|
|
|
|
gpio_lcd_active();
|
|
slcd_gpio_config();
|
|
|
|
// DATCTL
|
|
#ifdef CONFIG_FB_MXC_ASYNC_PANEL_IFC_16_BIT
|
|
// 16-bit 565 mode
|
|
cmd_param = 0x28;
|
|
#else
|
|
// 8-bit 666 mode
|
|
cmd_param = 0x08;
|
|
#endif
|
|
ipu_adc_write_cmd(disp, CMD, DATCTL, &cmd_param, 1);
|
|
|
|
// Sleep OUT
|
|
ipu_adc_write_cmd(disp, CMD, SLPOUT, 0, 0);
|
|
|
|
// Set display to white
|
|
// Setup page and column addresses
|
|
set_panel_region(disp, MXCFB_SCREEN_LEFT_OFFSET,
|
|
MXCFB_SCREEN_WIDTH + MXCFB_SCREEN_LEFT_OFFSET - 1,
|
|
0, MXCFB_SCREEN_HEIGHT - 1);
|
|
// Do RAM write cmd
|
|
ipu_adc_write_cmd(disp, CMD, RAMWR, 0, 0);
|
|
#ifdef CONFIG_FB_MXC_ASYNC_PANEL_IFC_16_BIT
|
|
for (i = 0; i < (MXCFB_SCREEN_WIDTH * MXCFB_SCREEN_HEIGHT); i++)
|
|
#else
|
|
for (i = 0; i < (MXCFB_SCREEN_WIDTH * MXCFB_SCREEN_HEIGHT * 3); i++)
|
|
#endif
|
|
ipu_adc_write_cmd(disp, DAT, 0xFFFF, 0, 0);
|
|
|
|
// Pause 80 ms
|
|
mdelay(80);
|
|
|
|
// Display ON
|
|
ipu_adc_write_cmd(disp, CMD, DISON, 0, 0);
|
|
// Pause 200 ms
|
|
mdelay(200);
|
|
|
|
pr_debug("initialized panel\n");
|
|
}
|
|
|
|
#ifdef PARTIAL_REFRESH
|
|
static irqreturn_t mxcfb_sys2_eof_irq_handler(int irq, void *dev_id)
|
|
{
|
|
ipu_channel_params_t params;
|
|
struct fb_info *fbi = dev_id;
|
|
struct mxcfb_info *mxc_fbi = fbi->par;
|
|
uint32_t stat[2], seg_size;
|
|
uint32_t lsb, msb;
|
|
uint32_t update_height, start_line, start_addr, end_line, end_addr;
|
|
uint32_t stride_pixels = (fbi->fix.line_length * 8) /
|
|
fbi->var.bits_per_pixel;
|
|
|
|
ipu_adc_get_snooping_status(&stat[0], &stat[1]);
|
|
//DPRINTK("snoop status = 0x%08X%08X\n", stat[1], stat[0]);
|
|
|
|
if (!stat[0] && !stat[1]) {
|
|
dev_err(fbi->device, "error no bus snooping bits set\n");
|
|
return IRQ_HANDLED;
|
|
}
|
|
ipu_disable_irq(IPU_IRQ_ADC_SYS2_EOF);
|
|
|
|
lsb = ffs(stat[0]);
|
|
if (lsb) {
|
|
lsb--;
|
|
} else {
|
|
lsb = ffs(stat[1]);
|
|
lsb += 32 - 1;
|
|
}
|
|
msb = fls(stat[1]);
|
|
if (msb) {
|
|
msb += 32;
|
|
} else {
|
|
msb = fls(stat[0]);
|
|
}
|
|
|
|
seg_size = mxc_fbi->snoop_window_size / 64;
|
|
|
|
start_addr = lsb * seg_size; // starting address offset
|
|
start_line = start_addr / fbi->fix.line_length;
|
|
start_addr = start_line * fbi->fix.line_length; // Addr aligned to line
|
|
start_addr += fbi->fix.smem_start;
|
|
|
|
end_addr = msb * seg_size; // ending address offset
|
|
end_line = end_addr / fbi->fix.line_length;
|
|
end_line++;
|
|
|
|
if (end_line > fbi->var.yres) {
|
|
end_line = fbi->var.yres;
|
|
}
|
|
|
|
update_height = end_line - start_line;
|
|
dev_dbg(fbi->device, "updating rows %d to %d, start addr = 0x%08X\n",
|
|
start_line, end_line, start_addr);
|
|
|
|
ipu_uninit_channel(ADC_SYS1);
|
|
params.adc_sys1.disp = mxc_fbi->disp_num;
|
|
params.adc_sys1.ch_mode = WriteTemplateNonSeq;
|
|
params.adc_sys1.out_left = MXCFB_SCREEN_LEFT_OFFSET;
|
|
params.adc_sys1.out_top = start_line;
|
|
ipu_init_channel(ADC_SYS1, ¶ms);
|
|
|
|
ipu_init_channel_buffer(ADC_SYS1, IPU_INPUT_BUFFER,
|
|
bpp_to_pixfmt(fbi->var.bits_per_pixel),
|
|
MXCFB_SCREEN_WIDTH,
|
|
update_height,
|
|
stride_pixels,
|
|
IPU_ROTATE_NONE, (dma_addr_t) start_addr, 0,
|
|
0, 0);
|
|
ipu_enable_channel(ADC_SYS1);
|
|
ipu_select_buffer(ADC_SYS1, IPU_INPUT_BUFFER, 0);
|
|
ipu_enable_irq(IPU_IRQ_ADC_SYS1_EOF);
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static irqreturn_t mxcfb_sys1_eof_irq_handler(int irq, void *dev_id)
|
|
{
|
|
ipu_disable_irq(IPU_IRQ_ADC_SYS1_EOF);
|
|
ipu_disable_channel(ADC_SYS1, false);
|
|
|
|
ipu_enable_channel(ADC_SYS2);
|
|
ipu_enable_irq(IPU_IRQ_ADC_SYS2_EOF);
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
#endif
|
|
|
|
/*!
|
|
* Function to initialize Asynchronous Display Controller. It also initilizes
|
|
* the ADC System 1 channel. Configure ADC display 0 parallel interface for
|
|
* the panel.
|
|
*
|
|
* @param fbi framebuffer information pointer
|
|
*/
|
|
static void mxcfb_init_panel(struct fb_info *fbi)
|
|
{
|
|
int msb;
|
|
int panel_stride;
|
|
ipu_channel_params_t params;
|
|
struct mxcfb_info *mxc_fbi = (struct mxcfb_info *)fbi->par;
|
|
|
|
#ifdef CONFIG_FB_MXC_ASYNC_PANEL_IFC_16_BIT
|
|
uint32_t pix_fmt = IPU_PIX_FMT_RGB565;
|
|
ipu_adc_sig_cfg_t sig = { 0, 0, 0, 0, 0, 0, 0, 0,
|
|
IPU_ADC_BURST_WCS,
|
|
IPU_ADC_IFC_MODE_SYS80_TYPE2,
|
|
16, 0, 0, IPU_ADC_SER_NO_RW
|
|
};
|
|
mxc_fbi->disp_num = DISP0;
|
|
#elif defined(CONFIG_FB_MXC_ASYNC_PANEL_IFC_8_BIT)
|
|
uint32_t pix_fmt = IPU_PIX_FMT_RGB666;
|
|
ipu_adc_sig_cfg_t sig = { 0, 0, 0, 0, 0, 0, 0, 0,
|
|
IPU_ADC_BURST_WCS,
|
|
IPU_ADC_IFC_MODE_SYS80_TYPE2,
|
|
8, 0, 0, IPU_ADC_SER_NO_RW
|
|
};
|
|
mxc_fbi->disp_num = DISP0;
|
|
#else
|
|
uint32_t pix_fmt = IPU_PIX_FMT_RGB565;
|
|
ipu_adc_sig_cfg_t sig = { 0, 1, 0, 0, 0, 0, 0, 0,
|
|
IPU_ADC_BURST_SERIAL,
|
|
IPU_ADC_IFC_MODE_5WIRE_SERIAL_CLK,
|
|
16, 0, 0, IPU_ADC_SER_NO_RW
|
|
};
|
|
fbi->disp_num = DISP1;
|
|
#endif
|
|
|
|
#ifdef PARTIAL_REFRESH
|
|
if (ipu_request_irq(IPU_IRQ_ADC_SYS2_EOF, mxcfb_sys2_eof_irq_handler, 0,
|
|
MXCFB_NAME, fbi) != 0) {
|
|
dev_err(fbi->device, "Error registering SYS2 irq handler.\n");
|
|
return;
|
|
}
|
|
|
|
if (ipu_request_irq(IPU_IRQ_ADC_SYS1_EOF, mxcfb_sys1_eof_irq_handler, 0,
|
|
MXCFB_NAME, fbi) != 0) {
|
|
dev_err(fbi->device, "Error registering SYS1 irq handler.\n");
|
|
return;
|
|
}
|
|
ipu_disable_irq(IPU_IRQ_ADC_SYS1_EOF);
|
|
ipu_disable_irq(IPU_IRQ_ADC_SYS2_EOF);
|
|
#endif
|
|
// Init DI interface
|
|
msb = fls(MXCFB_SCREEN_WIDTH);
|
|
if (!(MXCFB_SCREEN_WIDTH & ((1UL << msb) - 1)))
|
|
msb--; // Already aligned to power 2
|
|
panel_stride = 1UL << msb;
|
|
ipu_adc_init_panel(mxc_fbi->disp_num,
|
|
MXCFB_SCREEN_WIDTH + MXCFB_SCREEN_LEFT_OFFSET,
|
|
MXCFB_SCREEN_HEIGHT,
|
|
pix_fmt, panel_stride, sig, XY, 0, VsyncInternal);
|
|
|
|
ipu_adc_init_ifc_timing(mxc_fbi->disp_num, true,
|
|
190, 17, 104, 190, 5000000);
|
|
ipu_adc_init_ifc_timing(mxc_fbi->disp_num, false, 123, 17, 68, 0, 0);
|
|
|
|
// Needed to turn on ADC clock for panel init
|
|
memset(¶ms, 0, sizeof(params));
|
|
params.adc_sys1.disp = mxc_fbi->disp_num;
|
|
params.adc_sys1.ch_mode = WriteTemplateNonSeq;
|
|
params.adc_sys1.out_left = MXCFB_SCREEN_LEFT_OFFSET;
|
|
params.adc_sys1.out_top = MXCFB_SCREEN_TOP_OFFSET;
|
|
ipu_init_channel(ADC_SYS1, ¶ms);
|
|
|
|
_init_panel(mxc_fbi->disp_num);
|
|
init_channel_template(mxc_fbi->disp_num);
|
|
}
|
|
|
|
int mxcfb_set_refresh_mode(struct fb_info *fbi, int mode,
|
|
struct mxcfb_rect *update_region)
|
|
{
|
|
unsigned long start_addr;
|
|
int ret_mode;
|
|
uint32_t dummy;
|
|
ipu_channel_params_t params;
|
|
struct mxcfb_rect rect;
|
|
struct mxcfb_info *mxc_fbi = (struct mxcfb_info *)fbi->par;
|
|
uint32_t stride_pixels = (fbi->fix.line_length * 8) /
|
|
fbi->var.bits_per_pixel;
|
|
uint32_t memsize = fbi->fix.smem_len;
|
|
|
|
if (mxc_fbi->cur_update_mode == mode)
|
|
return mode;
|
|
|
|
ret_mode = mxc_fbi->cur_update_mode;
|
|
|
|
ipu_disable_irq(IPU_IRQ_ADC_SYS1_EOF);
|
|
ipu_adc_set_update_mode(ADC_SYS1, IPU_ADC_REFRESH_NONE, 0, 0, 0);
|
|
#ifdef PARTIAL_REFRESH
|
|
ipu_disable_irq(IPU_IRQ_ADC_SYS2_EOF);
|
|
ipu_adc_set_update_mode(ADC_SYS2, IPU_ADC_REFRESH_NONE, 0, 0, 0);
|
|
#endif
|
|
|
|
ipu_disable_channel(ADC_SYS1, true);
|
|
ipu_clear_irq(IPU_IRQ_ADC_SYS1_EOF);
|
|
#ifdef PARTIAL_REFRESH
|
|
ipu_disable_channel(ADC_SYS2, true);
|
|
ipu_clear_irq(IPU_IRQ_ADC_SYS2_EOF);
|
|
#endif
|
|
ipu_adc_get_snooping_status(&dummy, &dummy);
|
|
|
|
mxc_fbi->cur_update_mode = mode;
|
|
|
|
switch (mode) {
|
|
case MXCFB_REFRESH_OFF:
|
|
if (ipu_adc_set_update_mode(ADC_SYS1, IPU_ADC_REFRESH_NONE,
|
|
0, 0, 0) < 0)
|
|
dev_err(fbi->device, "Error enabling auto refesh.\n");
|
|
if (ipu_adc_set_update_mode(ADC_SYS2, IPU_ADC_REFRESH_NONE,
|
|
0, 0, 0) < 0)
|
|
dev_err(fbi->device, "Error enabling auto refesh.\n");
|
|
#if 0
|
|
ipu_init_channel_buffer(ADC_SYS2, IPU_INPUT_BUFFER,
|
|
bpp_to_pixfmt(fbi->var.bits_per_pixel),
|
|
1, 1, 4,
|
|
IPU_ROTATE_NONE,
|
|
fbi->fix.smem_start,
|
|
fbi->fix.smem_start, 0, 0);
|
|
ipu_enable_channel(ADC_SYS2);
|
|
ipu_select_buffer(ADC_SYS2, IPU_INPUT_BUFFER, 0);
|
|
ipu_select_buffer(ADC_SYS2, IPU_INPUT_BUFFER, 1);
|
|
msleep(10);
|
|
#endif
|
|
ipu_uninit_channel(ADC_SYS1);
|
|
#ifdef PARTIAL_REFRESH
|
|
ipu_uninit_channel(ADC_SYS2);
|
|
#endif
|
|
break;
|
|
case MXCFB_REFRESH_PARTIAL:
|
|
#ifdef PARTIAL_REFRESH
|
|
ipu_adc_get_snooping_status(&dummy, &dummy);
|
|
|
|
params.adc_sys2.disp = DISP0;
|
|
params.adc_sys2.ch_mode = WriteTemplateNonSeq;
|
|
params.adc_sys2.out_left = 0;
|
|
params.adc_sys2.out_top = 0;
|
|
ipu_init_channel(ADC_SYS2, ¶ms);
|
|
|
|
if (ipu_adc_set_update_mode(ADC_SYS1, IPU_ADC_REFRESH_NONE,
|
|
0, 0, 0) < 0) {
|
|
dev_err(fbi->device, "Error enabling auto refesh.\n");
|
|
}
|
|
if (ipu_adc_set_update_mode
|
|
(ADC_SYS2, IPU_ADC_AUTO_REFRESH_SNOOP, 30,
|
|
fbi->fix.smem_start, &memsize) < 0) {
|
|
dev_err(fbi->device, "Error enabling auto refesh.\n");
|
|
}
|
|
mxc_fbi->snoop_window_size = memsize;
|
|
|
|
ipu_init_channel_buffer(ADC_SYS2, IPU_INPUT_BUFFER,
|
|
bpp_to_pixfmt(fbi->var.bits_per_pixel),
|
|
1, 1, 4,
|
|
IPU_ROTATE_NONE,
|
|
fbi->fix.smem_start, 0, 0, 0);
|
|
|
|
params.adc_sys1.disp = mxc_fbi->disp_num;
|
|
params.adc_sys1.ch_mode = WriteTemplateNonSeq;
|
|
params.adc_sys1.out_left = MXCFB_SCREEN_LEFT_OFFSET;
|
|
params.adc_sys1.out_top = MXCFB_SCREEN_TOP_OFFSET;
|
|
ipu_init_channel(ADC_SYS1, ¶ms);
|
|
|
|
ipu_init_channel_buffer(ADC_SYS1, IPU_INPUT_BUFFER,
|
|
bpp_to_pixfmt(fbi->var.bits_per_pixel),
|
|
MXCFB_SCREEN_WIDTH, MXCFB_SCREEN_HEIGHT,
|
|
stride_pixels, IPU_ROTATE_NONE,
|
|
fbi->fix.smem_start, 0, 0, 0);
|
|
ipu_enable_channel(ADC_SYS1);
|
|
ipu_select_buffer(ADC_SYS1, IPU_INPUT_BUFFER, 0);
|
|
ipu_enable_irq(IPU_IRQ_ADC_SYS1_EOF);
|
|
break;
|
|
#endif
|
|
case MXCFB_REFRESH_AUTO:
|
|
if (update_region == NULL) {
|
|
update_region = ▭
|
|
rect.top = 0;
|
|
rect.left = 0;
|
|
rect.height = MXCFB_SCREEN_HEIGHT;
|
|
rect.width = MXCFB_SCREEN_WIDTH;
|
|
}
|
|
params.adc_sys1.disp = mxc_fbi->disp_num;
|
|
params.adc_sys1.ch_mode = WriteTemplateNonSeq;
|
|
params.adc_sys1.out_left = MXCFB_SCREEN_LEFT_OFFSET +
|
|
update_region->left;
|
|
params.adc_sys1.out_top = MXCFB_SCREEN_TOP_OFFSET +
|
|
update_region->top;
|
|
ipu_init_channel(ADC_SYS1, ¶ms);
|
|
|
|
// Address aligned to line
|
|
start_addr = update_region->top * fbi->fix.line_length;
|
|
start_addr += fbi->fix.smem_start;
|
|
start_addr += update_region->left * fbi->var.bits_per_pixel / 8;
|
|
|
|
ipu_init_channel_buffer(ADC_SYS1, IPU_INPUT_BUFFER,
|
|
bpp_to_pixfmt(fbi->var.bits_per_pixel),
|
|
update_region->width,
|
|
update_region->height, stride_pixels,
|
|
IPU_ROTATE_NONE, start_addr, 0, 0, 0);
|
|
ipu_enable_channel(ADC_SYS1);
|
|
ipu_select_buffer(ADC_SYS1, IPU_INPUT_BUFFER, 0);
|
|
|
|
if (ipu_adc_set_update_mode
|
|
(ADC_SYS1, IPU_ADC_AUTO_REFRESH_SNOOP, 30,
|
|
fbi->fix.smem_start, &memsize) < 0)
|
|
dev_err(fbi->device, "Error enabling auto refesh.\n");
|
|
|
|
mxc_fbi->snoop_window_size = memsize;
|
|
|
|
break;
|
|
}
|
|
return ret_mode;
|
|
}
|
|
|
|
/*
|
|
* Open the main framebuffer.
|
|
*
|
|
* @param fbi framebuffer information pointer
|
|
*
|
|
* @param user Set if opened by user or clear if opened by kernel
|
|
*/
|
|
static int mxcfb_open(struct fb_info *fbi, int user)
|
|
{
|
|
int retval = 0;
|
|
struct mxcfb_info *mxc_fbi = fbi->par;
|
|
|
|
if ((retval = wait_event_interruptible(mxcfb_drv_data.suspend_wq,
|
|
(mxcfb_drv_data.suspended ==
|
|
false))) < 0) {
|
|
return retval;
|
|
}
|
|
|
|
mxc_fbi->open_count++;
|
|
|
|
retval = mxcfb_blank(FB_BLANK_UNBLANK, fbi);
|
|
return retval;
|
|
}
|
|
|
|
/*
|
|
* Close the main framebuffer.
|
|
*
|
|
* @param fbi framebuffer information pointer
|
|
*
|
|
* @param user Set if opened by user or clear if opened by kernel
|
|
*/
|
|
static int mxcfb_release(struct fb_info *fbi, int user)
|
|
{
|
|
int retval = 0;
|
|
struct mxcfb_info *mxc_fbi = fbi->par;
|
|
|
|
if ((retval = wait_event_interruptible(mxcfb_drv_data.suspend_wq,
|
|
(mxcfb_drv_data.suspended ==
|
|
false))) < 0) {
|
|
return retval;
|
|
}
|
|
|
|
--mxc_fbi->open_count;
|
|
if (mxc_fbi->open_count == 0) {
|
|
retval = mxcfb_blank(FB_BLANK_POWERDOWN, fbi);
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
/*
|
|
* 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;
|
|
struct mxcfb_info *mxc_fbi = (struct mxcfb_info *)info->par;
|
|
|
|
// Set framebuffer id to IPU display number.
|
|
strcpy(fix->id, "DISP0 FB");
|
|
fix->id[4] = '0' + mxc_fbi->disp_num;
|
|
|
|
// Init settings based on the panel size
|
|
fix->line_length = MXCFB_SCREEN_WIDTH * var->bits_per_pixel / 8;
|
|
|
|
fix->type = FB_TYPE_PACKED_PIXELS;
|
|
fix->accel = FB_ACCEL_NONE;
|
|
fix->visual = FB_VISUAL_TRUECOLOR;
|
|
fix->xpanstep = 0;
|
|
fix->ypanstep = 0;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* 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;
|
|
int mode;
|
|
|
|
if ((retval = wait_event_interruptible(mxcfb_drv_data.suspend_wq,
|
|
(mxcfb_drv_data.suspended ==
|
|
false))) < 0) {
|
|
return retval;
|
|
}
|
|
|
|
mode = mxcfb_set_refresh_mode(fbi, MXCFB_REFRESH_OFF, NULL);
|
|
|
|
mxcfb_set_fix(fbi);
|
|
|
|
if (mode != MXCFB_REFRESH_OFF) {
|
|
#ifdef PARTIAL_REFRESH
|
|
mxcfb_set_refresh_mode(fbi, MXCFB_REFRESH_PARTIAL, NULL);
|
|
#else
|
|
mxcfb_set_refresh_mode(fbi, MXCFB_REFRESH_AUTO, NULL);
|
|
#endif
|
|
}
|
|
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 *fbi)
|
|
{
|
|
if (var->xres > MXCFB_SCREEN_WIDTH)
|
|
var->xres = MXCFB_SCREEN_WIDTH;
|
|
if (var->yres > MXCFB_SCREEN_HEIGHT)
|
|
var->yres = MXCFB_SCREEN_HEIGHT;
|
|
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 = default_bpp;
|
|
}
|
|
|
|
switch (var->bits_per_pixel) {
|
|
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;
|
|
}
|
|
|
|
var->height = -1;
|
|
var->width = -1;
|
|
var->grayscale = 0;
|
|
var->nonstd = 0;
|
|
|
|
var->pixclock = -1;
|
|
var->left_margin = -1;
|
|
var->right_margin = -1;
|
|
var->upper_margin = -1;
|
|
var->lower_margin = -1;
|
|
var->hsync_len = -1;
|
|
var->vsync_len = -1;
|
|
|
|
var->vmode = FB_VMODE_NONINTERLACED;
|
|
var->sync = 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;
|
|
}
|
|
|
|
/*
|
|
* mxcfb_blank():
|
|
* Blank the display.
|
|
*/
|
|
static int mxcfb_blank(int blank, struct fb_info *fbi)
|
|
{
|
|
int retval = 0;
|
|
struct mxcfb_info *mxc_fbi = fbi->par;
|
|
|
|
dev_dbg(fbi->device, "blank = %d\n", blank);
|
|
|
|
if ((retval = wait_event_interruptible(mxcfb_drv_data.suspend_wq,
|
|
(mxcfb_drv_data.suspended ==
|
|
false))) < 0) {
|
|
return retval;
|
|
}
|
|
|
|
mxc_fbi->blank = blank;
|
|
|
|
switch (blank) {
|
|
case FB_BLANK_POWERDOWN:
|
|
case FB_BLANK_VSYNC_SUSPEND:
|
|
case FB_BLANK_HSYNC_SUSPEND:
|
|
case FB_BLANK_NORMAL:
|
|
mxcfb_set_refresh_mode(fbi, MXCFB_REFRESH_OFF, NULL);
|
|
break;
|
|
case FB_BLANK_UNBLANK:
|
|
mxcfb_set_refresh_mode(fbi, MXCFB_REFRESH_DEFAULT, NULL);
|
|
break;
|
|
}
|
|
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_open = mxcfb_open,
|
|
.fb_release = mxcfb_release,
|
|
.fb_set_par = mxcfb_set_par,
|
|
.fb_check_var = mxcfb_check_var,
|
|
.fb_setcolreg = mxcfb_setcolreg,
|
|
.fb_fillrect = cfb_fillrect,
|
|
.fb_copyarea = cfb_copyarea,
|
|
.fb_imageblit = cfb_imageblit,
|
|
.fb_blank = mxcfb_blank,
|
|
};
|
|
|
|
/*!
|
|
* 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)
|
|
{
|
|
u32 msb;
|
|
u32 offset;
|
|
struct mxcfb_info *mxcfbi = fbi->par;
|
|
|
|
fbi->fix.smem_len = fbi->var.xres_virtual * fbi->var.yres_virtual * 4;
|
|
|
|
// Set size to power of 2.
|
|
msb = fls(fbi->fix.smem_len);
|
|
if (!(fbi->fix.smem_len & ((1UL << msb) - 1)))
|
|
msb--; // Already aligned to power 2
|
|
if (msb < 11)
|
|
msb = 11;
|
|
mxcfbi->alloc_size = (1UL << msb) * 2;
|
|
|
|
mxcfbi->alloc_start_vaddr = dma_alloc_coherent(fbi->device,
|
|
mxcfbi->alloc_size,
|
|
&mxcfbi->
|
|
alloc_start_paddr,
|
|
GFP_KERNEL | GFP_DMA);
|
|
|
|
if (mxcfbi->alloc_start_vaddr == 0) {
|
|
dev_err(fbi->device, "Unable to allocate framebuffer memory\n");
|
|
return -ENOMEM;
|
|
}
|
|
dev_dbg(fbi->device, "allocated fb memory @ paddr=0x%08X, size=%d.\n",
|
|
(uint32_t) mxcfbi->alloc_start_paddr, mxcfbi->alloc_size);
|
|
|
|
offset =
|
|
((mxcfbi->alloc_size / 2) - 1) & ~((mxcfbi->alloc_size / 2) - 1);
|
|
fbi->fix.smem_start = mxcfbi->alloc_start_paddr + offset;
|
|
dev_dbg(fbi->device, "aligned fb start @ paddr=0x%08lX, size=%u.\n",
|
|
fbi->fix.smem_start, fbi->fix.smem_len);
|
|
|
|
fbi->screen_base = mxcfbi->alloc_start_vaddr + offset;
|
|
|
|
/* Clear the screen */
|
|
memset(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)
|
|
{
|
|
struct mxcfb_info *mxc_fbi = (struct mxcfb_info *)fbi->par;
|
|
|
|
dma_free_coherent(fbi->device, mxc_fbi->alloc_size,
|
|
mxc_fbi->alloc_start_vaddr,
|
|
mxc_fbi->alloc_start_paddr);
|
|
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;
|
|
|
|
/*
|
|
* Fill in fb_info structure information
|
|
*/
|
|
fbi->var.xres = fbi->var.xres_virtual = MXCFB_SCREEN_WIDTH;
|
|
fbi->var.yres = fbi->var.yres_virtual = MXCFB_SCREEN_HEIGHT;
|
|
fbi->var.activate = FB_ACTIVATE_NOW;
|
|
mxcfb_check_var(&fbi->var, fbi);
|
|
|
|
mxcfb_set_fix(fbi);
|
|
|
|
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;
|
|
}
|
|
|
|
/*!
|
|
* 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 *mxc_fbi;
|
|
int ret;
|
|
|
|
platform_set_drvdata(pdev, &mxcfb_drv_data);
|
|
|
|
/*
|
|
* Initialize FB structures
|
|
*/
|
|
fbi = mxcfb_init_fbinfo(&pdev->dev, &mxcfb_ops);
|
|
if (!fbi) {
|
|
ret = -ENOMEM;
|
|
goto err0;
|
|
}
|
|
mxcfb_drv_data.fbi = fbi;
|
|
mxc_fbi = fbi->par;
|
|
|
|
mxcfb_drv_data.suspended = false;
|
|
init_waitqueue_head(&mxcfb_drv_data.suspend_wq);
|
|
|
|
/*
|
|
* Allocate memory
|
|
*/
|
|
ret = mxcfb_map_video_memory(fbi);
|
|
if (ret < 0) {
|
|
goto err1;
|
|
}
|
|
|
|
mxcfb_init_panel(fbi);
|
|
|
|
/*
|
|
* Register framebuffer
|
|
*/
|
|
ret = register_framebuffer(fbi);
|
|
if (ret < 0) {
|
|
goto err2;
|
|
}
|
|
|
|
dev_info(&pdev->dev, "%s registered\n", MXCFB_NAME);
|
|
|
|
return 0;
|
|
|
|
err2:
|
|
mxcfb_unmap_video_memory(fbi);
|
|
err1:
|
|
if (&fbi->cmap)
|
|
fb_dealloc_cmap(&fbi->cmap);
|
|
framebuffer_release(fbi);
|
|
err0:
|
|
return ret;
|
|
}
|
|
|
|
#ifdef CONFIG_PM
|
|
/*!
|
|
* Power management hooks. Note that we won't be called from IRQ context,
|
|
* unlike the blank functions above, so we may sleep.
|
|
*/
|
|
|
|
/*!
|
|
* Suspends the framebuffer and blanks the screen. Power management support
|
|
*
|
|
* @param pdev pointer to device structure.
|
|
* @param state state of the device.
|
|
*
|
|
* @return success
|
|
*/
|
|
static int mxcfb_suspend(struct platform_device *pdev, pm_message_t state)
|
|
{
|
|
struct mxcfb_data *drv_data = platform_get_drvdata(pdev);
|
|
struct fb_info *fbi = drv_data->fbi;
|
|
struct mxcfb_info *mxc_fbi = fbi->par;
|
|
|
|
drv_data->suspended = true;
|
|
|
|
if (mxc_fbi->blank == FB_BLANK_UNBLANK)
|
|
mxcfb_set_refresh_mode(fbi, MXCFB_REFRESH_OFF, NULL);
|
|
/* Display OFF */
|
|
ipu_adc_write_cmd(mxc_fbi->disp_num, CMD, DISOFF, 0, 0);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*!
|
|
* Resumes the framebuffer and unblanks the screen. Power management support
|
|
*
|
|
* @param pdev pointer to device structure.
|
|
*
|
|
* @return success
|
|
*/
|
|
static int mxcfb_resume(struct platform_device *pdev)
|
|
{
|
|
struct mxcfb_data *drv_data = platform_get_drvdata(pdev);
|
|
struct fb_info *fbi = drv_data->fbi;
|
|
struct mxcfb_info *mxc_fbi = fbi->par;
|
|
|
|
// Display ON
|
|
ipu_adc_write_cmd(mxc_fbi->disp_num, CMD, DISON, 0, 0);
|
|
drv_data->suspended = false;
|
|
|
|
if (mxc_fbi->blank == FB_BLANK_UNBLANK)
|
|
mxcfb_set_refresh_mode(fbi, MXCFB_REFRESH_DEFAULT, NULL);
|
|
wake_up_interruptible(&drv_data->suspend_wq);
|
|
|
|
return 0;
|
|
}
|
|
#else
|
|
#define mxcfb_suspend NULL
|
|
#define mxcfb_resume NULL
|
|
#endif
|
|
|
|
/*!
|
|
* This structure contains pointers to the power management callback functions.
|
|
*/
|
|
static struct platform_driver mxcfb_driver = {
|
|
.driver = {
|
|
.name = MXCFB_NAME,
|
|
},
|
|
.probe = mxcfb_probe,
|
|
.suspend = mxcfb_suspend,
|
|
.resume = mxcfb_resume,
|
|
};
|
|
|
|
/*!
|
|
* Device definition for the Framebuffer
|
|
*/
|
|
static struct platform_device mxcfb_device = {
|
|
.name = MXCFB_NAME,
|
|
.id = 0,
|
|
.dev = {
|
|
.coherent_dma_mask = 0xFFFFFFFF,
|
|
}
|
|
};
|
|
|
|
/*!
|
|
* 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
|
|
*/
|
|
static int mxcfb_init(void)
|
|
{
|
|
int ret = 0;
|
|
|
|
ret = platform_driver_register(&mxcfb_driver);
|
|
if (ret == 0) {
|
|
ret = platform_device_register(&mxcfb_device);
|
|
if (ret != 0) {
|
|
platform_driver_unregister(&mxcfb_driver);
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static void mxcfb_exit(void)
|
|
{
|
|
struct fb_info *fbi = dev_get_drvdata(&mxcfb_device.dev);
|
|
|
|
if (fbi) {
|
|
mxcfb_unmap_video_memory(fbi);
|
|
|
|
if (&fbi->cmap)
|
|
fb_dealloc_cmap(&fbi->cmap);
|
|
|
|
unregister_framebuffer(fbi);
|
|
framebuffer_release(fbi);
|
|
}
|
|
|
|
platform_device_unregister(&mxcfb_device);
|
|
platform_driver_unregister(&mxcfb_driver);
|
|
}
|
|
|
|
module_init(mxcfb_init);
|
|
module_exit(mxcfb_exit);
|
|
|
|
EXPORT_SYMBOL(mxcfb_set_refresh_mode);
|
|
|
|
MODULE_AUTHOR("Freescale Semiconductor, Inc.");
|
|
MODULE_DESCRIPTION("MXC Epson framebuffer driver");
|
|
MODULE_SUPPORTED_DEVICE("fb");
|