你的位置:首页 > 操作系统

[操作系统]S3C2440 LCD驱动(FrameBuffer)实例开发二(转)


开发板自带的LCD驱动是基于platform总线写的,所以如果要使其它的LCD能够在自己的开发板上跑起来,那么就先了解platform驱动的架构,下面简单记录下自己看platform驱动时体会,简单的说platform是一种虚拟总线,那么它也是一条总线,所以它分为3个部分,platform_bus,platform_device,platform_driver。在platform_device向platform_bus注册设备,platform_driver向platform_bus注册驱动,注册后在platform_bus中会有一条device链表和driver链表,platform_bus中match函数将匹配两者的名字,如果相同那就把驱动和设备进行绑定。Linux platform driver机制和传统的device driver机制(通过driver_register进行注册)相比,一个明显的优势在于platform机制将设备本身的资源注册进内核,由内核统一管理,在驱动中使用这些资源时通过platform device提供的标准结构进行申请并使用。这样提高了驱动和资源的独立性,并且具有较好的可移植性和安全性(这些标准接口是安全的)。下面举一个简单platform驱动的例子来分析platform_device和platform_driver是怎么联系,platform_driver是怎么使用platform_device提供硬件信息的。

led_dev.c

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <linux/module.h>
#include <linux/version.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/interrupt.h>
#include <linux/list.h>
#include <linux/timer.h>
#include <linux/init.h>
#include <linux/serial_core.h>
#include <linux/platform_device.h>
 
 
/* 分配/设置/注册一个platform_device */
 
static struct resource led_resource[] = {
    [0] = {
        .start = 0x56000010,             /* TQ2440的LED是GPB5,6,7,8, GPBCON地址是0x56000010 */
        .end   = 0x56000010 + 8 - 1,
        .flags = IORESOURCE_MEM,         /* 标识led控制器io端口*/
    },
    [1] = {
        .start = 5,                      /* LED1 */
        .end   = 5,
        .flags = IORESOURCE_IRQ,         /* 标识LED中断 */
    }
 
};
/* 必须提供realease函数,可以不实现 */
static void led_release(struct device * dev)
{
}
 
 
static struct platform_device led_dev = {
    .name         = "myled",   /* 设备名 */
    .id       = -1,            /* 一般设为-1,表示同样名字的设备只有一个 */
    .num_resources    = ARRAY_SIZE(led_resource), /*  资源数量*/
    .resource     = led_resource,
    .dev = {
        .release = led_release, /* 引用上面定义的资源 */
    },
};
 
static int led_dev_init(void)
{
    platform_device_register(&led_dev);/* 注册平台设备 */
    return 0;
}
 
static void led_dev_exit(void)
{
    platform_device_unregister(&led_dev);/*  注销平台设备*/
}
 
module_init(led_dev_init);
module_exit(led_dev_exit);
 
MODULE_LICENSE("GPL");




 

Led_drv.c

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include <linux/fs.h>
#include <linux/interrupt.h
#include <linux/irq.h>
#include <linux/sched.h>
#include <linux/pm.h>
#include <linux/sysctl.h>
#include <linux/proc_fs.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/input.h>
#include <linux/irq.h>
#include <asm/uaccess.h>
#include <asm/io.h>
 
static int major;
static struct class *cls;
static volatile unsigned long *gpio_con;
static volatile unsigned long *gpio_dat;
static int pin;
 
static int led_open(struct inode *inode, struct file *file)
{
    //printk("first_drv_open\n");
    /* 配置为输出 */
    *gpio_con &= ~(0x3<<(pin*2));
    *gpio_con |= (0x1<<(pin*2));
    return 0;  
}
 
static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
    int val;
 
    //printk("first_drv_write\n");
 
    copy_from_user(&val, buf, count); //    copy_to_user();
 
    if (val == 1)
    {
        // 点灯
        *gpio_dat &= ~(1<<pin);
    }
    else
    {
        // 灭灯
        *gpio_dat |= (1<<pin);
    }
     
    return 0;
}
static struct file_operations led_fops = {
    .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open   =   led_open,    
    .write  =   led_write,    
};
static int led_probe(struct platform_device *pdev)
{
    struct resource     *res;
    /* 分配/设置/注册一个platform_driver */
    /* 根据platform_device的资源进行ioremap */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    gpio_con = ioremap(res->start, res->end - res->start + 1);
    gpio_dat = gpio_con + 1;
 
    res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
    pin = res->start;
 
    /* 注册字符设备驱动程序 */
    printk("led_probe, found led\n");
 
    /* 注册设备,生成设备文件*/
    major = register_chrdev(0, "myled", &led_fops);
    cls = class_create(THIS_MODULE, "myled");
    class_device_create(cls, NULL, MKDEV(major, 0), NULL, "led"); /* /dev/led */
    return 0;
}
static int led_remove(struct platform_device *pdev)
{
    /* 卸载字符设备驱动程序 */
    /* iounmap */
    printk("led_remove, remove led\n");
    class_device_destroy(cls, MKDEV(major, 0));
    class_destroy(cls);
    unregister_chrdev(major, "myled");
    iounmap(gpio_con);
    return 0;
}
struct platform_driver led_drv = {
    .probe      = led_probe,
    .remove     = led_remove,
    .driver     = {
        .name   = "myled",
    }
};
static int led_drv_init(void)
{
    platform_driver_register(&led_drv);  /* 注册平台驱动 */
    return 0;
}
static void led_drv_exit(void)
{
    platform_driver_unregister(&led_drv); /* 注销平台驱动 */
}
module_init(led_drv_init);
module_exit(led_drv_exit);
MODULE_LICENSE("GPL");




 

这就是一个简单的platform驱动,这两个文件我们完全可以写道一个文件中去实现,现在看下由一个文件如何实现这个驱动:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
 
static struct class *seconddrv_class;
static struct class_device  *seconddrv_class_dev;
 
volatile unsigned long *gpfcon;
volatile unsigned long *gpfdat;
static int second_drv_open(struct inode *inode, struct file *file)
{
    /*
     * K1,K2,K3,K4对应GPF1、GPF4、GPF2、GPF0
     */
 
    /* 配置GPF1、GPF4、GPF2、GPF0为输入引脚 */
    *gpfcon &= ~((0x3<<(1*2)) | (0x3<<(4*2)) | (0x3<<(2*2)) | (0x3<<(0*2)));
    return 0;
}
ssize_t second_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
    /* 返回4个引脚的电平 */
    unsigned char key_vals[4];
    int regval;
    if (size != sizeof(key_vals))
        return -EINVAL;
 
    regval = *gpfdat;
    key_vals[0] = (regval & (1<<1)) ? 1 : 0;
    key_vals[1] = (regval & (1<<4)) ? 1 : 0;
    key_vals[2] = (regval & (1<<2)) ? 1 : 0;
    key_vals[3] = (regval & (1<<0)) ? 1 : 0;
 
    copy_to_user(buf, key_vals, sizeof(key_vals));
     
    return sizeof(key_vals);
}
static struct file_operations sencod_drv_fops = {
    .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open   =   second_drv_open,    
    .read   =   second_drv_read,      
};
int major;
static int second_drv_init(void)
{
    major = register_chrdev(0, "second_drv", &sencod_drv_fops);
 
    seconddrv_class = class_create(THIS_MODULE, "second_drv");
 
    seconddrv_class_dev = class_device_create(seconddrv_class, NULL, MKDEV(major, 0), NULL, "buttons"); /* /dev/buttons */
 
    gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
    gpfdat = gpfcon + 1;
 
    return 0;
}
static void second_drv_exit(void)
{
    unregister_chrdev(major, "second_drv");
    class_device_unregister(seconddrv_class_dev);
    class_destroy(seconddrv_class);
    iounmap(gpfcon);
    return 0;
}
module_init(second_drv_init);
module_exit(second_drv_exit);
MODULE_LICENSE("GPL");




 

     由此可见,如果由platform驱动去实现led驱动将会多出很多东西,而这些多出来的就是我们如何把一个驱动程序融合到platform里面,那既然用platform驱动要多写那么多东西那问什么还要写基于platform的驱动呢?我的理解是基于以下几个原因:如果一个设备挂在总线上,其结果是配套的sysfs结点,设备电源管理都成为可能;隔离了BSP和驱动,在BSP中定义platform设备和设备使用的资源,设备的具体配置信息,而在驱动中,只要通过API去获取资源和数据,做到了板相关代码与驱动代码的分离,使得驱动具有更好的可移植性和更好的扩展性和跨平台性;假如我们要实现一个LCD驱动,那么我们只需修改BSP相关的代码,platform基本上不需修改,避免重复着轮子的可能性;基于platformplatform机制将设备本身的资源注册进内核,由内核统一管理。

    上面platform的例子还没有完全按照linux platform的标准去写,因为阅读代码可知linux platform驱动把platform_device相关的代码放在一块,然后统一进行注册!

下面记录下如何把device添加到板级文件中(最开始还是建议写成两个文件,分别编译成模块加载,因为如果是放到板级文件中那么需要重新编译内核,这个将在编译上浪费很多时间)

步骤:

1. 在/arch/arm/plat-s3c24xx/devs.c中定义led 相关的设备及资源,代码如下:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static struct resource led_resource[] = {
    [0] = {
        .start = 0x56000010,             /* TQ2440的LED是GPB5,6,7,8, GPBCON地址是0x56000010 */
        .end   = 0x56000010 + 8 - 1,
        .flags = IORESOURCE_MEM,         /* 标识led控制器io端口*/
    },
    [1] = {
        .start = 5,                      /* LED1 */
        .end   = 5,
        .flags = IORESOURCE_IRQ,         /* 标识LED中断 */
    }
 
};
EXPORT_SYMBOL(s3c_device_lcd);




 

请注意最后一行是导出led平台设备,会在mach-smdk2440.c中添加到平台设备列表中。

2. 如果还需定义led的其它信息,那么在/arch/arm/plat-s3c2410/include/mach/fb.h 为led平台设备定义一个s3c2410fb_mach_info结果体。led很简单所以没有必要再去定义,但是后面讲的LCD的平台设备需要定义!因为仅仅通过resource,驱动还无法操作LCD。

3. 不要实现,只是记录下系统还会做什么事! 在mach-smdk2440.c中将会调用platform_add_devices(),注册平台设备。

好了对platform有了一定了解后,那么看看如果将LCD驱动写成linux设备模型的platform驱动,同样给出不使用platform架构是一个完整的字符设备驱动的lcd驱动,在给出一个符合platform模型的lcd驱动,通过这两个文件的对比,就可以知道在符合platform模型的lcd驱动中可以找到LCD驱动的全部信息,也会发现基本上所有plat_form驱动出了驱动本身的东西外,框架性的东西基本上一样的,所以如果理解了一两个platform驱动,那么以后写platform驱动或看platform代码跟不以flatform写出的代码是没有什么区别的!代码如下:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/fb.h>
#include <linux/init.h>
#include <linux/dma-mapping.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/wait.h>
#include <linux/platform_device.h>
#include <linux/clk.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <asm/div64.h>
#include <asm/mach/map.h>
#include <asm/arch/regs-lcd.h>
#include <asm/arch/regs-gpio.h>
#include <asm/arch/fb.h>
static int s3c_lcdfb_setcolreg(unsigned int regno, unsigned int red,
                 unsigned int green, unsigned int blue,
                 unsigned int transp, struct fb_info *info);
struct lcd_regs {
    unsigned long   lcdcon1;
    unsigned long   lcdcon2;
    unsigned long   lcdcon3;
    unsigned long   lcdcon4;
    unsigned long   lcdcon5;
    unsigned long   lcdsaddr1;
    unsigned long   lcdsaddr2;
    unsigned long   lcdsaddr3;
    unsigned long   redlut;
    unsigned long   greenlut;
    unsigned long   bluelut;
    unsigned long   reserved[9];
    unsigned long   dithmode;
    unsigned long   tpal;
    unsigned long   lcdintpnd;
    unsigned long   lcdsrcpnd;
    unsigned long   lcdintmsk;
    unsigned long   lpcsel;
};
 
static struct fb_ops s3c_lcdfb_ops = {
    .owner      = THIS_MODULE,
    .fb_setcolreg   = s3c_lcdfb_setcolreg,
    .fb_fillrect    = cfb_fillrect,
    .fb_copyarea    = cfb_copyarea,
    .fb_imageblit   = cfb_imageblit,
};
 
 
static struct fb_info *s3c_lcd;
static volatile unsigned long *gpbcon;
static volatile unsigned long *gpbdat;
static volatile unsigned long *gpccon;
static volatile unsigned long *gpdcon;
static volatile unsigned long *gpgcon;
static volatile struct lcd_regs* lcd_regs;
static u32 pseudo_palette[16];
 
 
/* from pxafb.c */
static inline unsigned int chan_to_field(unsigned int chan, struct fb_bitfield *bf)
{
    chan &= 0xffff;
    chan >>= 16 - bf->length;
    return chan << bf->offset;
}
 
 
static int s3c_lcdfb_setcolreg(unsigned int regno, unsigned int red,
                 unsigned int green, unsigned int blue,
                 unsigned int transp, struct fb_info *info)
{
    unsigned int val;
     
    if (regno > 16)
        return 1;
 
    /* 用red,green,blue三原色构造出val */
    val  = chan_to_field(red,   &info->var.red);
    val |= chan_to_field(green, &info->var.green);
    val |= chan_to_field(blue,  &info->var.blue);
     
    //((u32 *)(info->pseudo_palette))[regno] = val;
    pseudo_palette[regno] = val;
    return 0;
}
 
static int lcd_init(void)
{
    /* 1. 分配一个fb_info */
    s3c_lcd = framebuffer_alloc(0, NULL);
 
    /* 2. 设置 */
    /* 2.1 设置固定的参数 */
    strcpy(s3c_lcd->fix.id, "mylcd");
    s3c_lcd->fix.smem_len = 480*272*32/8;        /* TQ2440的LCD位宽是24,但是2440里会分配4字节即32位(浪费1字节) */
    s3c_lcd->fix.type     = FB_TYPE_PACKED_PIXELS;
    s3c_lcd->fix.visual   = FB_VISUAL_TRUECOLOR; /* TFT */
    s3c_lcd->fix.line_length = 480*4;
     
    /* 2.2 设置可变的参数 */
    s3c_lcd->var.xres           = 480;
    s3c_lcd->var.yres           = 272;
    s3c_lcd->var.xres_virtual   = 480;
    s3c_lcd->var.yres_virtual   = 272;
    s3c_lcd->var.bits_per_pixel = 32;
 
    /* RGB:565 */
    s3c_lcd->var.red.offset     = 16;
    s3c_lcd->var.red.length     = 8;
    s3c_lcd->var.green.offset   = 8;
    s3c_lcd->var.green.length   = 8;
    s3c_lcd->var.blue.offset    = 0;
    s3c_lcd->var.blue.length    = 8;
    s3c_lcd->var.activate       = FB_ACTIVATE_NOW;  
    /* 2.3 设置操作函数 */
    s3c_lcd->fbops              = &s3c_lcdfb_ops;
     
    /* 2.4 其他的设置 */
    s3c_lcd->pseudo_palette = pseudo_palette;
    //s3c_lcd->screen_base  = ;  /* 显存的虚拟地址 */
    s3c_lcd->screen_size   = 480*272*32/8;
    /* 3. 硬件相关的操作 */
    /* 3.1 配置GPIO用于LCD */
    gpbcon = ioremap(0x56000010, 8);
    gpbdat = gpbcon+1;
    gpccon = ioremap(0x56000020, 4);
    gpdcon = ioremap(0x56000030, 4);
    gpgcon = ioremap(0x56000060, 4);
    *gpccon  = 0xaaaaaaaa;   /* GPIO管脚用于VD[7:0],LCDVF[2:0],VM,VFRAME,VLINE,VCLK,LEND */
    *gpdcon  = 0xaaaaaaaa;   /* GPIO管脚用于VD[23:8] */
    *gpgcon |= (3<<8); /* GPG4用作LCD_PWREN */
     
    /* 3.2 根据LCD手册设置LCD控制器, 比如VCLK的频率等 */
    lcd_regs = ioremap(0x4D000000, sizeof(struct lcd_regs));
    /*
     * TQ2440 4.3英寸LCD手册为WXCAT43-TG6#001_V1.0.pdf第22、23页
     *
     * LCD手册和2440手册"Figure 15-6. TFT LCD Timing Example"一对比就知道参数含义了
     */
 
    /* bit[17:8]: VCLK = HCLK / [(CLKVAL+1) x 2], LCD手册P22 (Dclk=9MHz~15MHz)
     *            10MHz(100ns) = 100MHz / [(CLKVAL+1) x 2]
     *            CLKVAL = 4
     * bit[6:5]: 0b11, TFT LCD
     * bit[4:1]: 0b1101, 24 bpp for TFT
     * bit[0]  : 0 = Disable the video output and the LCD control signal.
     */
    lcd_regs->lcdcon1  = (4<<8) | (3<<5) | (0x0d<<1);
    /* 垂直方向的时间参数
     * bit[31:24]: VBPD, VSYNC之后再过多长时间才能发出第1行数据
     *             LCD手册 tvb=2
     *             VBPD=1
     * bit[23:14]: 多少行, 272, 所以LINEVAL=272-1=271
     * bit[13:6] : VFPD, 发出最后一行数据之后,再过多长时间才发出VSYNC
     *             LCD手册tvf=2, 所以VFPD=2-1=1
     * bit[5:0]  : VSPW, VSYNC信号的脉冲宽度, LCD手册tvp=10, 所以VSPW=10-1=9
     */
    lcd_regs->lcdcon2  = (1<<24) | (271<<14) | (1<<6) | (9<<0);
    /* 水平方向的时间参数
     * bit[25:19]: HBPD, VSYNC之后再过多长时间才能发出第1行数据
     *             LCD手册 thb=2
     *             HBPD=1
     * bit[18:8]: 多少列, 480, 所以HOZVAL=480-1=479
     * bit[7:0] : HFPD, 发出最后一行里最后一个象素数据之后,再过多长时间才发出HSYNC
     *             LCD手册thf=2, 所以HFPD=2-1=1
     */
    lcd_regs->lcdcon3 = (1<<19) | (479<<8) | (1<<0);
    /* 水平方向的同步信号
     * bit[7:0] : HSPW, HSYNC信号的脉冲宽度, LCD手册Thp=41, 所以HSPW=41-1=40
     */
    lcd_regs->lcdcon4 = 40;
 
    /* 信号的极性
     * bit[11]: 1=565 format, 对于24bpp这个不用设
     * bit[10]: 0 = The video data is fetched at VCLK falling edge
     * bit[9] : 1 = HSYNC信号要反转,即低电平有效
     * bit[8] : 1 = VSYNC信号要反转,即低电平有效
     * bit[6] : 0 = VDEN不用反转
     * bit[3] : 0 = PWREN输出0
     *
     * BSWP = 0, HWSWP = 0, BPP24BL = 0 : 当bpp=24时,2440会给每一个象素分配32位即4字节,哪一个字节是不使用的? 看2440手册P412
         * bit[12]: 0, LSB valid, 即最高字节不使用
     * bit[1] : 0 = BSWP
     * bit[0] : 0 = HWSWP
     */
    lcd_regs->lcdcon5 = (0<<10) | (1<<9) | (1<<8) | (0<<12) | (0<<1) | (0<<0);
    /* 3.3 分配显存(framebuffer), 并把地址告诉LCD控制器 */
    s3c_lcd->screen_base = dma_alloc_writecombine(NULL, s3c_lcd->fix.smem_len, &s3c_lcd->fix.smem_start, GFP_KERNEL); 
    lcd_regs->lcdsaddr1  = (s3c_lcd->fix.smem_start >> 1) & ~(3<<30);
    lcd_regs->lcdsaddr2  = ((s3c_lcd->fix.smem_start + s3c_lcd->fix.smem_len) >> 1) & 0x1fffff;
    lcd_regs->lcdsaddr3  = (480*32/16);  /* 一行的长度(单位: 2字节) */   
    //s3c_lcd->fix.smem_start = xxx;  /* 显存的物理地址 */
    /* 启动LCD */
    lcd_regs->lcdcon1 |= (1<<0); /* 使能LCD控制器 */
    lcd_regs->lcdcon5 |= (1<<3); /* 使能LCD本身: LCD_PWREN */
//  *gpbdat |= 1;     /* 输出高电平, 使能背光, TQ2440的背光电路也是通过LCD_PWREN来控制的 */
    /* 4. 注册 */
    register_framebuffer(s3c_lcd); 
    return 0;
}
 
static void lcd_exit(void)
{
    unregister_framebuffer(s3c_lcd);
    lcd_regs->lcdcon1 &= ~(1<<0); /* 关闭LCD控制器 */
    lcd_regs->lcdcon1 &= ~(1<<3); /* 关闭LCD本身 */
//  *gpbdat &= ~1;     /* 关闭背光 */
    dma_free_writecombine(NULL, s3c_lcd->fix.smem_len, s3c_lcd->screen_base, s3c_lcd->fix.smem_start);
    iounmap(lcd_regs);
    iounmap(gpbcon);
    iounmap(gpccon);
    iounmap(gpdcon);
    iounmap(gpgcon);
    framebuffer_release(s3c_lcd);
}
module_init(lcd_init);
module_exit(lcd_exit);
MODULE_LICENSE("GPL");




 

基于platform的代码在这就不贴出来了,太多了,可以参考/driver/videv/s3c2410fb.c